Setting Up Azure Key Vault with an Azure Website (Web API)

Series:
1. Securing React App with Azure AD
2. Setting Up Azure Key Vault with an Azure Website (Web API) (this post)
3. Running tenant specific operations (Create modern site, etc)

As you know from my previous post, I need to save a user password somewhere in order to be able to authenticate with the Office 365 tenant, even if we know that data stored in Cosmos DB is secure, the data there is plaintext, in this case I wanted to give a try to Azure Key Vault.

Azure Key Vault allows to store securely certificates, encryption keys, but also secrets or passwords. One of the advantages of Azure Key Vault is that you can secure the access to the vault to app themselves, so there is no way other APP or person can get access to it.

It works like an Azure AD App registration, its the same concept, you are basically allowing an external app (Azure Website/WEB API), to connect to your resource: Key Vault.

For this you will need the Azure CLI

The steps are:
1. Create your key vault in your region, not going to explain this here as the azure portal is pretty straightforward
2. Assign your web app an identity, the name parameter its what is front of the azurewebsite url: https://yourapp.azurewebsites.net

az webapp identity assign --name "webapi-app" --resource-group "DEV"  
  1. Take note of the principal id returned by the command.
{
  "identityIds": null,
  "principalId": "xxxxxxxx-922e-4635-b2f6-4f20c96b432b",
  "tenantId": "xxxxxxxx-c220-48a2-a73f-1177fa2c098e",
  "type": "SystemAssigned"
}
  1. Set a policy to your key vault, the name parameter is your keyvault name, and the object id parameter its the principal id from the previous command. Please note that in the command below, I am only assigning GET, LIST and SET permissions, but there are some more
az keyvault set-policy --name mykeyvault--object-id xxxxxxxx-922e-4635-b2f6-4f20c96b432b --secret-permissions get list set  
  1. If you want, use the below command to show the permissions given.
az keyvault secret show --id https://mykeyvault.vault.azure.net/secrets/test/568bb22e45f54cfba905f1fcc7666ca9  

Now thats it, you can use the Azure Key Vault SDK to get and set your keys.

In my case, as the application its supposed to manage multiple Office 365 tenants, whenever I register a new tenant in my Cosmos DB Application, what I need to do is to create a new Key Vault Secret, and whenever I need to use the tenant for an operation, like creating a site collection, then I read from the key vault.

For that I created the below helper class:

 public class KeyVaultHelper
    {
        public string Message { get; set; }
        public string SecretIdentifier { get; set; }
        public string SecretValue { get; set; }

        public async Task OnGetAsync(string url)
        {
            Message = "Your application description page.";
            int retries = 0;
            bool retry = false;
            try
            {
                /* The below 4 lines of code shows you how to use AppAuthentication library to fetch secrets from your Key Vault*/
                AzureServiceTokenProvider azureServiceTokenProvider = new AzureServiceTokenProvider();
                KeyVaultClient keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));
                var secret = await keyVaultClient.GetSecretAsync(url)
                        .ConfigureAwait(false);
                SecretValue = secret.Value;

                /* The below do while logic is to handle throttling errors thrown by Azure Key Vault. It shows how to do exponential backoff which is the recommended client side throttling*/
                do
                {
                    long waitTime = Math.Min(getWaitTime(retries), 2000000);
                    secret = await keyVaultClient.GetSecretAsync(url)
                        .ConfigureAwait(false);
                    SecretValue = secret.Value;

                    retry = false;
                }
                while (retry && (retries++ < 10));
            }
            /// <exception cref="KeyVaultErrorException">
            /// Thrown when the operation returned an invalid status code
            /// </exception>
            catch (KeyVaultErrorException keyVaultException)
            {
                Message = keyVaultException.Message;
                if ((int)keyVaultException.Response.StatusCode == 429)
                    retry = true;
            }
        }


        public async Task OnCreateAsync(string name, string value)
        {
            Message = "Your application description page.";
            int retries = 0;
            bool retry = false;
            try
            {
                /* The below 4 lines of code shows you how to use AppAuthentication library to set secrets from your Key Vault*/
                AzureServiceTokenProvider azureServiceTokenProvider = new AzureServiceTokenProvider();
                KeyVaultClient keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));
                var result = await keyVaultClient.SetSecretAsync(ConfigurationManager.AppSettings["VaultUrl"].ToString(), name, value)
                            .ConfigureAwait(false);
                SecretIdentifier = result.Id;


                /* The below do while logic is to handle throttling errors thrown by Azure Key Vault. It shows how to do exponential backoff which is the recommended client side throttling*/
                do
                {
                    long waitTime = Math.Min(getWaitTime(retries), 2000000);
                    result = await keyVaultClient.SetSecretAsync(ConfigurationManager.AppSettings["VaultUrl"].ToString(), name, value)
                        .ConfigureAwait(false);
                    Message = result.Id;
                    retry = false;
                }
                while (retry && (retries++ < 10));
            }
            /// <exception cref="KeyVaultErrorException">
            /// Thrown when the operation returned an invalid status code
            /// </exception>
            catch (KeyVaultErrorException keyVaultException)
            {
                Message = keyVaultException.Message;
                if ((int)keyVaultException.Response.StatusCode == 429)
                    retry = true;
            }
        }


        // This method implements exponential backoff incase of 429 errors from Azure Key Vault
        private static long getWaitTime(int retryCount)
        {
            long waitTime = ((long)Math.Pow(2, retryCount) * 100L);
            return waitTime;
        }

        // This method fetches a token from Azure Active Directory which can then be provided to Azure Key Vault to authenticate
        public async Task<string> GetAccessTokenAsync()
        {
            var azureServiceTokenProvider = new AzureServiceTokenProvider();
            string accessToken = await azureServiceTokenProvider.GetAccessTokenAsync("https://vault.azure.net");
            return accessToken;
        }
    }

Its a bunch of methods that allow you to get or set information from the key vault, so that when we need them we can just easily call a method.

And finally, in our TenantController, we can use something like this:

 [HttpPut]
        public async Task<IHttpActionResult> PutTenant([FromBody]SharepointTenant tenant)
        {
            try
            {
                using (var context = new OfficeDevPnP.Core.AuthenticationManager().GetSharePointOnlineAuthenticatedContextTenant(tenant.TestSiteCollectionUrl, tenant.Email, tenant.Password))
                {
                    context.Load(context.Web, p => p.Title);
                    context.ExecuteQuery();
                };

                string domainUrl = tenant.TestSiteCollectionUrl;
                string tenantName = domainUrl.Split('.')[0].Remove(0,8);

                tenant.Active = false;
                tenant.TenantName = tenantName;

                KeyVaultHelper keyVaultHelper = new KeyVaultHelper();
                await keyVaultHelper.OnCreateAsync(tenant.TenantName, tenant.Password);
                tenant.Password = "Hidden";
                tenant.SecretIdentifier = keyVaultHelper.SecretIdentifier;

                var tenantStore = CosmosStoreFactory.CreateForEntity<SharepointTenant>();

                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }

                var added = await tenantStore.AddAsync(tenant);
                return StatusCode(HttpStatusCode.NoContent);
            }
            catch (System.Exception ex)
            {
                return BadRequest("Invalid information entered, cant authenticate.");
            }
        }

What does this method does? Well basically we receive a tenant information from our React Front End, the first thing is we need to verify if the password is correct by doing any operation, like getting a site collection title.

Then later, we instantiate our KeyVaultHelper with the tenant name and the tenant pasword, yes like a Key, Value Pair. As you can see this method is async, but once its finished, the SecretIdentifier will hold an URL that we will save on CosmosDB to get the password later when we need it.

As you can see the Secret Identifider holds the URL of the keyvault to get the secret.

Now, lets suppose you want to create a site collection for that tenant, easy, we can get the information from the tenant from CosmosDB, and then with the secret identifier, we can get the secret from the Key Vault.

   [HttpGet]
        public async Task<List<TenantManagementWebApi.Entities.SiteCollection>> Get()
        {
            var tenant = await TenantHelper.GetActiveTenant();
            var siteCollectionStore = CosmosStoreFactory.CreateForEntity<TenantManagementWebApi.Entities.SiteCollection>();
            await siteCollectionStore.RemoveAsync(x => x.Title != string.Empty); // Removes all the entities that match the criteria
            string domainUrl = tenant.TestSiteCollectionUrl;
            string tenantName = domainUrl.Split('.')[0];
            string tenantAdminUrl =  tenantName + "-admin.sharepoint.com";

            KeyVaultHelper keyVaultHelper = new KeyVaultHelper();
            await keyVaultHelper.OnGetAsync(tenant.SecretIdentifier);

            using (var context = new OfficeDevPnP.Core.AuthenticationManager().GetSharePointOnlineAuthenticatedContextTenant(tenantAdminUrl, tenant.Email, keyVaultHelper.SecretValue))
            {

The key part in this code is the .OnGetAsync, and we are using the secret identifier url to get exactly what we need.