Azure Function that uses Managed Identity to connect to Azure Storage

This short tutorial is more or less a proof of concept to show how to connect an Azure Function with a
private Azure Storage in an easy way.

The easy way is by using Managed Identity (formerly known as Managed Service Identity). What Managed
Identity (MI) does, is giving your Azure Function an Identity in your subscription. This makes it very
simple to make your app identify itself and get the needed tokens, for say, Storage or Keyvault access.

The whole idea of using MI from an azure function to a storage was actually pitched by Marco Kerstens, and
when I wanted to spend some time on it - he already had it working. But because of the beauty of the
solution I think it's worth sharing, so I more or less recreated his solution.

As this tutorial is meant to to show the mechanics and steps behind it, so I am going to set it up via the
portal.

1. let's create a new azure function in Visual Studio. Click new Project and search for Function (and click
Next)

select function project

2. Give the project a name (e.g. MSIFunctionWriteToStorage ), a location and a solution and click next.

3. Make sure you select Azure Functions v2.0 (.Net Core) and keep the access rights on "Function", pick
HTTPTrigger as the trigger

setup function project

4. A new project is created and you can publish it straight away, by right clicking the project name and
clicking "Publish..."

publish function

5. Now go and look it up in the azure portal. It's under web app, but also directly under functions. Once
found, click on the main project name and select overview

portal function overview

6. Click on Identity in the second column, under the header Networking

7. On the page that you see now, you're in the tab System Assigned, and just click on the on
button under status and Save. This tells azure that this Function now has an Identity.

8. Go to homepage of the portal and create a storage account, pick a v2. Inside the storage account, click
on blob and create a blob container (name it e.g. files). Select this container and click on Access Control
(IAM)."

create storage

9. On the Access Control screen, you click Add Role assignment and in the blade that pops up, you can pick
the role you want to add for a specific actor. In this case we want to add a storage blob data contributor
(meaning read/write/delete-access), for an App Service, and to be precise, the function app you added the
identity for. Search it by typing it's first letter(s).

create storage

10. And with all this in place, we can go back to visual studio and edit the function's code, just replace
it with the code below

using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.WindowsAzure.Storage.Auth;
using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.WindowsAzure.Storage.Blob;
using Microsoft.WindowsAzure.Storage;
using System;

namespace MSI_Blob
{
public static class Function1
{
[FunctionName("Function1")]
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;


var azureServiceTokenProvider = new AzureServiceTokenProvider();
string accessToken = await azureServiceTokenProvider.GetAccessTokenAsync("https://storage.azure.com");

log.LogInformation("accessToken : retrieved");


// create the credential, using the
var tokenCredential = new TokenCredential(accessToken);
var storageCredentials = new StorageCredentials(tokenCredential);

log.LogInformation("credentials : created");
var storageURL = "https://msistoragebe13.blob.core.windows.net/files/";
var fileName = "append";
var Uri = new Uri(storageURL + fileName);
var blob = new CloudAppendBlob(Uri, storageCredentials);

log.LogInformation($"blobfile : setup {0}", Uri);

if (!(await blob.ExistsAsync()))
{
await blob.CreateOrReplaceAsync(AccessCondition.GenerateIfNotExistsCondition(), null, null);
}

await blob.AppendTextAsync(name);

var fileName2 = "regular.txt";
var Uri2 = new Uri(storageURL + fileName2);
var blob2 = new CloudBlockBlob(Uri2, storageCredentials);

await blob2.UploadTextAsync(name);

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

11. Publish the new code and go to the azure portal's function app, click on function1, fill in some JSON in
post method { "name" : "martijn" }
and hit the test button. This should create 2 new files in
the storage, respectively append and example. The first time you do this test these files will be mostly
the same - though you cant edit the append file from the storage explorer in the portal.
However the second time, the append file should contain the name value of the first test as well as the
second test. The example file will just be the last name.

How does the code work? When the function1 gets triggered, it first gets the name from the post or get
query. This is the default sample behavior, then it will create a new AzureServiceTokenProvider.
When you don't provide any parameters on this constructor, it will try four providers. One of them is the
Managed Identity provider. As this function is set up for this, it will use this when we ask it in the next
line to provide us with an accesstoken for azure storage.

With this access token for storage the credentials are created, and those credentials are being sent to the
URI (the container of the storage in combination with the filename). First for the appendblob (a blob
specialized for appending to), and after that in a similar fashion for the BlockBlob (basically a regular
blob).
When the CloudBlobs are referenced, it checks if the blob already exists or should be created and append
the string to the file. Or in case of the BlockBlob it just overwrites the file.

Eventually it still has the sample behavior to return a result that tells the user (us, in case of using
test) what the name parameter was, or if it wasn't set, how to set it.