Handle secrets in Azure functions configuration

Problem

T

here are moments when you must include connection strings in your Azure function, such as for a ServiceBusTrigger. While developing locally, local.settings.json serves as a proper configuration solution. However, upon deployment to Azure, it becomes necessary to use Azure Functions configuration. This scenario presents a challenge due to configurations being stored in plain text, thereby posing a significant security risk. Anyone with access to the Azure portal can view these secret values. The Azure team acknowledges this issue and there are several open GitHub issues addressing it as of the writing of this article. This has always been a concern for me, and just last week, I discovered a solution I would like to share with you.
Preferred architecture
The best way to keep secrets in Azure safe is by using Azure Key Vault. I like to keep things organized with Azure App Configuration, which offers a service-centric approach to manage application settings and feature flags. It simplifies the deployment process and keeps configurations separate from the code. Plus, it’s cool because you can link up secrets from the Key Vault. So, what I’m aiming to do is set up my function to grab its connection strings right from Azure App Configuration, which in turn pulls the real secrets from Azure Key Vault.

Here is the configuration in Azure app configuration:

Solution
Usually you connect to Azure app configuration at runtime and load requested values into your IConfiguration using ConfigurationBuilder. You can do the same in Azure functions, but you are not able to use these values in several triggers. They need to be embedded right at the function level. Luckily there is new feature that will allow us to reference the secret value. It is still in preview at the time of writing this article.
Now, let’s dive into the config values in our Azure App Configuration. We’re aiming to pull the ConnectionStringFromAppConfig first. To achieve it, we will use special syntax used for referencing value from App configuration.
@Microsoft.AppConfiguration(Endpoint=https://app-config-functions-secret-demo.azconfig.io; Key=ConnectionStringFromAppConfig)
  • Endpoint=endpoint; Endpoint is the required part of the reference string. The value for Endpoint should have the url of your App Configuration resource.
  • Key=keyName; Key forms the required part of the reference string. Value for Key should be the name of the Key that you want to assign to the App setting.
  • Label=label; The Label part is optional in reference string. Label should be the value of Label for the Key specified in Key.

We’ll use RBAC and managed identity to set up authorization for our function to access the configuration and secret. This involves adding the role of App configuration data reader to Azure app configuration and Key vault secret user to Key vault. This setup works perfectly for a System Assigned Managed Identity. However, for a User Assigned Managed Identity, we need to specify which identity the application should use. This is done by adjusting the function’s keyVaultReferenceIdentity property to the resource ID of the user-assigned identity.

userAssignedIdentityResourceId=$(az identity show -g functions-secret-config-demo -n function-identity --query id -o tsv)
az functionapp update --resource-group functions-secret-config-demo --name function-secrets-demo --set keyVaultReferenceIdentity=${userAssignedIdentityResourceId}
Conclusion

And there we have it – our values are now successfully loaded in the application, and we’ve securely moved our secrets to the ideal spot: Azure Key Vault. You’ll notice the Source column pointing to the App configuration reference. A green tick icon is your sign of success, showing that we’ve successfully retrieved the value.

Current configuration inside the application:
{
    "deployment_branch": "master",
    "SCM_TRACE_LEVEL": "Verbose",
    "SCM_COMMAND_IDLE_TIMEOUT": "60",
    "SCM_LOGSTREAM_TIMEOUT": "7200",
    "SCM_BUILD_ARGS": "",
    "FUNCTIONS_RUNTIME_SCALE_MONITORING_ENABLED": "0",
    "ScmType": "None",
    "AzureWebJobsStorage": "DefaultEndpointsProtocol=https;AccountName=functionssecretconf881a;AccountKey=someky;EndpointSuffix=core.windows.net",
    "CONNECTION_STRING__KEY_VAULT": "super secret connection string",
    "SCM_USE_LIBGIT2SHARP_REPOSITORY": "0",
    "CONNECTION_STRING__APP_CONFIG": "my connections string taken from app onfiguration",
    "WEBSITE_SLOT_NAME": "Production",
    "WEBSITE_AUTH_LOGOUT_PATH": "/.auth/logout",
    "WEBSITE_AUTH_AUTO_AAD": "False",
    "REMOTEDEBUGGINGVERSION": "16.0.33328.57",
    "FUNCTIONS_EXTENSION_VERSION": "~4",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "WEBSITE_SITE_NAME": "function-secrets-demo",
    "WEBSITE_AUTH_ENABLED": "False",
    "WEBSITE_USE_PLACEHOLDER_DOTNETISOLATED": "1",
    "CONNECTION_STRING__PLAINTEXT": "my plaintext connection string"
}

 

Install PS module into C# Azure function or app service

Cloud services make it easy to create and host your applications. But these benefits come with a cost. For instance underlying hardware is often shared between customers. This brings the cost down but on the other hand, you loose some control over your environment. This is necessary because otherwise other users could disrupt run of your application.

The problem

Today I encounter a problem, where I need to create C# API, which will get results from Powershell library and return these results to the user. API is hosted as Azure app service web app and the library is published on PowerShell Gallery. I am running Powershell inside my C# code using System.Management.Automation NuGet package.

First thing that comes to my mind is install the module to my runspace using command Install-Module <module name>. This requires administrator rights and it just doesn’t work. I don’t like the idea downloading the powershell module from gallery each time anyway.

The solution

Every module published in PowerShell Gallery can also be downloaded. You can use some kind of package manager tool or do it manually. In general I like the idea of having specific tested version of the module packed with my application. After the download we ended up with package.nupkg file. But how can we integrate it into our Powershell runspace? The .nupkg filetype is nothing more than an archive. You can extract it with anything that can extract ZIP like archives. The result is shown on image bellow.

extracted nupkg

Cool! Now we ended up having familiar .psd1 file, which can be easily imported into the runspace. In short to use third party powershell module in Azure app service you have to take care of its download, extract it and then import it into your session.

using (Runspace runspace = RunspaceFactory.CreateRunspace())
{
    runspace.Open();
    PowerShell powershell = PowerShell.Create();
    powershell.Runspace = runspace;

    powershell.Commands.AddCommand("Set-ExecutionPolicy")
        .AddParameter("Scope", "Process")
        .AddParameter("ExecutionPolicy", "RemoteSigned");
    powershell.Invoke();
    powershell.Commands.Clear();

    powershell.Commands.AddCommand("Import-Module")
        .AddArgument(@".\PS\ExchangeOnlineManagement.psd1");
    powershell.Invoke();
    powershell.Commands.Clear();
    
    //you can use imported module now
}

DefaultAzureIdentity And Its Various Credential Types

Modern applications consist of lots of independent components. Microservice architecture brings great benefits but it also has its downsides. Developers must take care of communication between various parts of the system and make it secure and authenticated. One of the preferred ways is to give your component identity from Azure Active Directory (AAD) and utilize the use of AAD tokens. This demo shows various ways how to retrieve identity from application context using a single line of code and get sample secrets from the Azure Key Vault. This all is done with the help of DefaultAzureCredential class from Azure.Identity NuGet package.

The whole demo can be cloned from my GitHub repo.

Prerequisites

We will create an instance of the Azure Key vault. As the second step, we insert the value `supersecurevalue` as a secret with the key `mylittlesecret`. This all is done with the help of Azure CLI.

az keyvault create --location westeurope --name azureidentityvault --resource-group identitytest  
az keyvault secret set --name mylittlesecret --value supersecurevalue --vault-name azureidentityvault
DefaultAzureCredential

There are various identities we want to use for our application during different stages of the development cycle. For example, one for development, one for integration testing, and one for production. For sure we don’t want to have a separate code section for each environment. Azure.Identity NuGet package makes retrieving identity unified. The following example retrieves our secret from the created Key vault (uses C# 9 and top-level statements). 

using System;  
using Azure.Identity;  
using Azure.Security.KeyVault.Secrets;  
  
const string keyvaultName = "azureidentityvault";  
const string secretKey = "mylittlesecret";  
  
var credential = new DefaultAzureCredential();  
  
var client = new SecretClient(new Uri($"https://{keyvaultName}.vault.azure.net/"), credential);  
KeyVaultSecret secret = client.GetSecret(secretKey);  
Console.WriteLine(secret.Value);
DefaultAzureCredential combines some classes, that are used to retrieve AAD identity. It tries to initialize them one by one (in this order). The first successfully initialized credential is used:
  1. EnvironmentCredential
  2. ManagedIdentityCredential
  3. SharedTokenCacheCredential
  4. VisualStudioCredential
  5. VisualStudioCodeCredential
  6. AzureCliCredential
  7. InteractiveBrowserCredential
When your application runs in a production environment your identity will be probably retrieved with one of first three classes. When you debug your application locally, on the other hand, managed identity or environment variables could not be available. We will talk about each of these types of credentials from bottom to the top in the following sections.

 

InteractiveBrowserCredential

This type of credentials opens the default browser and lets the user do an interactive sign in. If you enter the credentials of the account that created the key vault, you should see the secret. Retrieve credentials using this code:

var credential = new InteractiveBrowserCredential();

Keep in mind, that DefaultAzureCredential excludes interactive login by default. If you want to use it, you have to initialize it with the includeInteractiveCredentials option set to true.

var credential = new DefaultAzureCredential(includeInteractiveCredentials: true);
AzureCliCredential
var credential = new AzureCliCredential();

If you are in the terminal environment, you can log to Azure CLI using the az login command. An application running in the same terminal will use the identity provided during login.

 

VisualStudioCodeCredential
var credential = new new VisualStudioCodeCredential();

Azure Visual studio extensions required you to be logged in to show your Azure resources. You need an Azure account extension for this purpose. It providers various commands on how to perform sign-in. Just hit F1 and start typing “Azure Sign In”. VisualStudioCodeCredential takes this identity and uses it for the identity of our application during runtime.

visual studio code sign in
VisualStudioCredential
var credential = new new VisualStudioCredential();

This option is very similar to the previous one. It differs only in the IDE and the way of providing credentials to it. In the “big” Visual studio you find the login form in Tools > Options > Azure service authentication.

visual studio sign in
SharedTokenCacheCredential
var credential = new new SharedTokenCacheCredential();

Many Microsoft applications use Azure single sign-on. This class uses identity, that was already stored in the local cache by one of them.

ManagedIdentityCredential
var credential = new ManagedIdentityCredential();  
Managed identity is a great way how to secure your service in production. The application will receive an identity managed by Azure itself. You do not even have access to credentials.
In this case, administrators can use role-based access control to set up permissions for other resources.
We can demonstrate this by creating a simple HTTP-based Azure function. First, create storage and the function app itself. 
az storage account create --name identityfunctionstorage --resource-group identitytest  
az functionapp create --name identityfunctiondemo --resource-group identitytest --storage-account identityfunctionstorage --consumption-plan-location westeurope

Then we command Azure to assign managed identity for our Azure function (response is just for illustration).

az functionapp identity assign --name identityfunctiondemo --resource-group mirotest

{  
  "principalId": "3fedf722-7c5d-426f-9d35-d985d3eb59bc",  
  "tenantId": "8d099a24-312e-4bb5-8fe4-aed67b7c4921",  
  "type": "SystemAssigned",  
  "userAssignedIdentities": null  
}

Our application now has Azure identity with ID 3fedf722-7c5d-426f-9d35-d985d3eb59bc. The last configuration step is to add permission for our newly created application to be able to retrieve secrets from our Key vault. 

az keyvault set-policy --name azureidentityvault --object-id 3fedf722-7c5d-426f-9d35-d985d3eb59bc --secret-permission get

The code of the function app is in folder Azure.Identity.Demo.Function of this repository. After successful deployment, you will see the Invoke URL. Enter it in the browser and you will see the value of the secret as a response.

cd Azure.Identity.Demo.Function  

func azure functionapp publish identityfunctiondemo  
  
Microsoft (R) Build Engine version 16.8.0+126527ff1 for .NET  
Copyright (C) Microsoft Corporation. All rights reserved.  
  
  Determining projects to restore...  
  All projects are up-to-date for restore.  
  Azure_Identity_Demo_Function -> D:\working\Azure.Identity.Demo.Function\bin\publish\bin\Azure_Identity_Demo_Function.dll  
  
Build succeeded.  
    0 Warning(s)  
    0 Error(s)  
  
Time Elapsed 00:00:02.22  
  
  
Getting site publishing info...  
Creating archive for current directory...  
Uploading 4,06 MB [###############################################################################]  
Upload completed successfully.  
Deployment completed successfully.  
Syncing triggers...  
Functions in identityfunctiondemo:  
    IdentityHttpFunction - [httpTrigger]  
        Invoke url: https://identityfunctiondemo.azurewebsites.net/api/identityhttpfunction?code=QOLVCOC0FNtMIgN5bRur4sQSoEXkGraUovGmcsnULKPBiHuJXVKQwg==
EnvironmentCredential
var credential = new EnvironmentCredential();
The most universal way of providing an Azure identity for your application is to use system environment variables. You can configure them in your virtual machine, in your build server, in your cloud hosting, pass them into your docker image, and many other places. You can choose between using a regular user account or an application credential secured by a secret or certificate.
  • AZURE_TENANT_ID – The Azure Active Directory tenant (directory) ID.
  • AZURE_CLIENT_ID – The client (application) ID of an App Registration in the tenant.
  • AZURE_CLIENT_SECRET – A client secret that was generated for the App Registration.
  • AZURE_CLIENT_CERTIFICATE_PATH – A path to the certificate and private key pair in PEM or PFX format, which can authenticate the App Registration.
  • AZURE_USERNAME – The username, also known as upn, of an Azure Active Directory user account.
  • AZURE_PASSWORD – The password of the Azure Active Directory user account. Note this does not support accounts with MFA enabled.
Conclusion

DefaultAzureCredential class makes the everyday life of developers much easier. By typing a single line of code, we can provide a unified solution for providing identity. It adapts well to various environments starting from local debugging in IDE, continuing with build runners, and ending up in production cloud hosting.

Authorize ASP.NET Core app by Azure AD groups using Graph API

Business websites use AD groups as authentication mechanism quite often. Before cloud era, ASP.NET translated AD groups into roles out of the box. This is no longer possible with Azure AD. At least not so simple. Now there are 2 ways you can check group membership:

  • Set Azure AD to include security groups membership information into JWT token.
  • Query Graph API for user groups.

There are many tutorials describing the first approach. It is easy and effective, however it has its limitations. If the user is member of a lot of groups, size of the token will grow. There is limit 200 group ids in one JWT token. Error message appears, that points you to Graph API, if you try to request token for user with more than 200 groups. This sample demonstrates how to obtain users AD groups from Graph API and assign ASP.NET roles based on these groups. Roles are then stored in cookie, so only first request queries Graph API.

The whole sample can be cloned from my GitHub repo.

How to run this sample

You need access to Azure AD to register your application and check ids of groups.

Register Azure AD application
  1. Create new Azure AD application and set its reply URL. I won’t cover this in detail.
  2. Set up a secret in Certificates & secrets tab.
  3. In API permissions tab, add permission Microsoft Graph -> GroupMember.Read.All. User.Read is present by default. Don’t forget to grant admin consent.

Fill in information about your app into AzureAD section of appsettings.json file.

"AzureAD": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "<your domain>",
    "TenantId": "<your tenant id>",
    "ClientId": "<your client id>",
    "ClientSecret": "<your client secret>"
  },

You would want to place your secret somewhere safer in production application.

Assign ASP.NET roles to your Azure AD groups

Find guid of your Azure AD groups. In the AuthorizationGroups section of appsettings.json file replace key-value pairs with group id as key and target role as value. You can add as many as you want.

"AuthorizationGroups": {
  "5b99527f-947b-4e8d-aad5-404f8d39008c": "examplerole1",
  "2bd89580-1d95-4a9a-98c2-a7a150168cba": "examplerole2"
},
Set up endpoints authorization and run the application

There are 3 endpoints:

  • / Default endpoint. Requires only to be logged in.
  • /roletest Requires role to grant access.
  • /accessdenied Redirect destination in case of failed authorization.

In Startup.cs modify { Roles = "examplerole1" } to match one of roles specified in previous step.

app.UseEndpoints(endpoints =>
  {
      endpoints.MapGet("/", async context =>
      {
          await context.Response.WriteAsync("Im authorized (no required role).");
      }).RequireAuthorization();

      endpoints.MapGet("/roletest", async context =>
      {
          await context.Response.WriteAsync("You passed the role test!");
      }).RequireAuthorization(new AuthorizeAttribute() { Roles = "examplerole1" });

      endpoints.MapGet("/accessdenied", async context =>
      {
          await context.Response.WriteAsync("Access denied!");
      });
  });

Run the application.

How does it work
Azure AD authentication

I used Microsoft.AspNetCore.Authentication.AzureAD.UI NuGet package. Startup.cs file changes:

services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
    .AddAzureAD(options => Configuration.Bind("AzureAD", options));
app.UseAuthentication();
app.UseAuthorization();

This package takes care of setting up Open Id Connect and Cookies.

 

Graph API

Class GraphService.cs takes care of all operations against Graph API. Method CheckMemberGroupsAsync gets collection of group ids and returns only ids, that user is member of. This is done by CheckMemberGroups Graph API method.

public async Task<IEnumerable<string>> CheckMemberGroupsAsync(IEnumerable<string> groupIds)
{
    //You can check up to a maximum of 20 groups per request (see graph api doc).
    var batchSize = 20;

    var tasks = new List<Task<IDirectoryObjectCheckMemberGroupsCollectionPage>>();
    foreach (var groupsBatch in groupIds.Batch(batchSize))
    {
        tasks.Add(_client.Me.CheckMemberGroups(groupsBatch).Request().PostAsync());
    }
    await Task.WhenAll(tasks);

    return tasks.SelectMany(x => x.Result.ToList());
}

Information about which user groups to check is taken from user context. That’s why GraphServiceClient must be created on behalf of user with it’s token. I’ve create factory method CreateOnBehalfOfUserAsync for this purpose.

public static async Task<GraphService> CreateOnBehalfOfUserAsync(string userToken, IConfiguration configuration)
{
    var clientApp = ConfidentialClientApplicationBuilder
        .Create(configuration["AzureAD:ClientId"])
        .WithTenantId(configuration["AzureAD:TenantId"])
        .WithClientSecret(configuration["AzureAD:ClientSecret"])
        .Build();

    var authResult = await clientApp
        .AcquireTokenOnBehalfOf(new[] { "User.Read", "GroupMember.Read.All" }, new UserAssertion(userToken))
        .ExecuteAsync();

    GraphServiceClient graphClient = new GraphServiceClient(
        "https://graph.microsoft.com/v1.0",
        new DelegateAuthenticationProvider(async (requestMessage) =>
        {
            requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", authResult.AccessToken);
        }));

    return new GraphService(graphClient);
}

 

Intercepting authentication flow and adding custom claims

OpenId exposes OnTokenValidated event. We can use returned token before authentication is finished. It is needed to create Graph API client, that will act on behalf of actual user.

  • Load key-value pairs of group ids and target roles from section AuthorizationGroups of configuration.
  • Create Grap API service on behalf of actual user.
  • Check which groups from configuration is user member of.
  • Create role claims from returned entries.
  • Add these claims to current user.

Added claims are stored in cookie, so other requests do not trigger this event.

services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
    {
        options.Events = new OpenIdConnectEvents
        {
            OnTokenValidated = async ctx =>
            {
                var roleGroups = new Dictionary<string, string>();
                Configuration.Bind("AuthorizationGroups", roleGroups);

                var graphService = await GraphService.CreateOnBehalfOfUserAsync(ctx.SecurityToken.RawData, Configuration);
                var memberGroups = await graphService.CheckMemberGroupsAsync(roleGroups.Keys);

                var claims = memberGroups.Select(groupGuid => new Claim(ClaimTypes.Role, roleGroups[groupGuid]));
                var appIdentity = new ClaimsIdentity(claims);
                ctx.Principal.AddIdentity(appIdentity);
            }
        };
    });