get-the-solution

Create an Azure App registrations daemon app

By
on Regards: C#; Azure; Infrastructure;

Introduction

Picture this: you’re running an on-premises Linux or Windows service that interacts with a variety of Azure resources. Blob Storage, App Configuration, Azure Web Apps - you name it, your service uses it. Traditionally, accessing these resources would require managing a multitude of secrets provided by each resource. This means juggling these credentials within your app and Azure itself. If any of these credentials change, you could find yourself in a worst-case scenario of needing to adjust and redeploy your app.

But what if there was a way to eliminate the need for managing these credentials? Enter the world of Azure App Registration.

Azure App Registration allows your app to “run” as an Azure Identity. This identity is granted access to various resources such as Blob Storage, App Configuration, and more. The result? You only need to maintain the credentials of the Managed Identity. This approach is also necessary if your app needs to access the Graph API or a REST endpoint secured with Azure AD.

Azure App Registration differentiates between PublicClientApplication and ConfidentialClientApplication. The former is typically a client app, like a mobile app, that isn’t capable of protecting any credentials. Our focus, however, is on ConfidentialClientApplication. This type of application runs on a server and is likely a web app, web API, or even a service/daemon application. 1 2

Workflow and Acquiring Tokens

To access any Azure Resources (like the Azure App Configuration which our App Registration was granted access) we acquire an access token of our App Registration. This access token can be used to access for example the App Configuration, GraphApi, Blob Storage and so on. Just keep in mind that the associated Managed Idendity of the App Registration requires granted access to those Azure Resources. Whenever we interact with those azure resources with an api request we add the acquired token to request header. When requesting an access token you can do that by supplying a scope. You can recognize it later from the following pattern api:\appresource\.default. Because the access token can expire we need to renew it after it expired (We will not consider this scenario at the moment).

Create a daemon app / ConfidentialClientApplication

  1. In Azure go to Home and select your Azure Active Directory. Select App registration: Home > Default Directory | App registrations

  2. Select New registration

  3. Enter a name (for example azureappreg) and select Accounts in this organizational directory only (Default Directory only - Single tenant)

  4. You should now see a new page: Home > Default Directory | azureappreg

  5. In the left menu select: Expose an Api and under the title “Scopes defined by this API” press Add a scope

  6. Enter an Application Scope. Instead of the guid you can enter api://azureappregScope

  7. Next enter Scope name (for example azureappreg.Read), a consent display name for example Read and a consent description. Finish with Add Scope.

  8. In the left menu select Certificates & secrets. Select Client secrets. Select New Secret and set a description and expiration date. Store the value of the Secret as you will later not be able to access it again.

  9. The Microsoft Graph Api is a good way to test our setup. So lets go to the left menu and select API Permissions. You will see that Microsoft Graph with User.Read with Delegated was automatically added when we created the App registration. Because Delegated (your application needs to access the API as the signed-in user) permissions aren’t supported for daemon apps, we need to add a new permission.

  10. Select Add Permission. Select Microsoft Graph and Application permissions. Under Permissions select (Application) Application.Read.All and (User) User.Read.All. Press Add Permission.

  11. Press the button Grant admin consent and confirm with Yes.

  12. Its time to test our setup. Go to the left menu and select Overview. Obtain the Guid values from Application (client) ID, Directory (tenant) ID and Application ID URI.

To test if the registered app can query the graph api create a console project with:

using Azure.Core; //nuget Azure.Core
using System.Net.Http.Headers;
using System.Text.Json;

Console.WriteLine("Hello, World!");

var clientid = "replace with Guid of Application (client) ID";
var clientSecret = "replace with secret from step 8";
var tenantid = "replace with Guid of Directory (tenant) ID";

var accessToken =await GetAccessTokenByScopeAsync(tenantid, "https://graph.microsoft.com/.default", clientid, clientSecret);
await GetApplicationsByGraphApiAsync(accessToken);

async Task<AccessToken> GetAccessTokenByScopeAsync(string tenantid, string scope, string clientId, string registeredAppSecrect)
{
    //https://docs.microsoft.com/en-us/graph/auth-v2-service#4-get-an-access-token
    var formUrlEncodedContent = new FormUrlEncodedContent(new[]
        {
                new KeyValuePair<string, string>("client_id", clientId),
                new KeyValuePair<string, string>("client_secret", registeredAppSecrect),
                new KeyValuePair<string, string>("scope", scope),
                new KeyValuePair<string, string>("grant_type", "client_credentials")
            });
    HttpClient httpClient = new HttpClient();
    var responseMessage = await httpClient.PostAsync($"https://login.microsoftonline.com/{tenantid}/oauth2/v2.0/token", formUrlEncodedContent);
    var tokenResponse = JsonSerializer.Deserialize<TokenResponse>(await responseMessage.Content.ReadAsStringAsync());
    if (tokenResponse == null) throw new InvalidOperationException($"{nameof(TokenResponse)} expected!");
    return new AccessToken(tokenResponse.access_token, DateTimeOffset.Now + TimeSpan.FromSeconds(tokenResponse.expires_in));
}
async Task GetApplicationsByGraphApiAsync(AccessToken accessToken)
{
    if (DateTimeOffset.Now > accessToken.ExpiresOn)
    {
        //call again GetAccessTokenByScopeAsync
    }
    HttpClient httpClient = new HttpClient();
    var defaultRequetHeaders = httpClient.DefaultRequestHeaders;
    if (defaultRequetHeaders.Accept == null || !defaultRequetHeaders.Accept.Any(m => m.MediaType == "application/json"))
    {
        httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    }
    defaultRequetHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Token);
    var responseMessage = await httpClient.GetAsync($"https://graph.microsoft.com/v1.0/applications/");
    var outputString = await responseMessage.Content.ReadAsStringAsync();
    using var jDoc = JsonDocument.Parse(outputString);
    outputString = JsonSerializer.Serialize(jDoc, new JsonSerializerOptions { WriteIndented = true });
    Console.WriteLine(outputString);

    responseMessage = await httpClient.GetAsync($"https://graph.microsoft.com/v1.0/users/");
    outputString = await responseMessage.Content.ReadAsStringAsync();
    Console.WriteLine(outputString);
    //Calling the /me endpoint requires a signed-in user and therefore a delegated permission.
    //Application permissions are not supported when using the /me endpoint.
    //responseMessage = await httpClient.GetAsync($"https://graph.microsoft.com/v1.0/me/");
    //outputString = await responseMessage.Content.ReadAsStringAsync();
    //Console.WriteLine(outputString);
}
/// <summary>
/// https://docs.microsoft.com/en-us/graph/auth-v2-service#token-response
/// </summary>
public class TokenResponse
{
    public string token_type { get; set; }
    public int expires_in { get; set; }
    public int ext_expires_in { get; set; }
    public string access_token { get; set; }
}

The call await GetAccessTokenByScopeAsync(tenantid, "https://graph.microsoft.com/.default", clientid, clientSecret); will request a token as the registered app (in this example azureappreg) from the graph api. We use the returned token in GetApplicationsByGraphApiAsync to access the graph api for querying the applications and users. The query against https://graph.microsoft.com/v1.0/applications/ should output the registered applications which were created. If you would like to receive in addition the applications permissions you need to adjust the manifest of the application registration. See here. It’s maybe worth mentioning that the registered app gets an access token for a request azure service even though it doesn’t have any privileges to interact with the resource. Maybe you also noticed the expiration of the AccessToken. If it expired you need to request a new Token.

Read Configuration from App Configuration

To give the app access to the App Configuration do the following:

  1. Create or switch to your App Configuration in azure for example get-app-configuration | Identity. Make sure Managed Identity is switched on (System assigned | Status on).
  2. In the left menu of the App Configuration open Access Control (IAM). Select Add role assignment under “Grant access to this resource”.
  3. Choose for Role App Configuration Data Reader. On the next tab Members select User, group, or service principal and press “+Select Members” and search for the name of registered app (in this sample the name was azureappreg you will find the name also in the overview section of the registered app). To complete the operation press the button Review + assign.
  4. Go to the overview of the App Configuration and copy the endpoint.

In the previous console project add the following method.

async Task GetAppConfigurationAsync(AccessToken accessToken)
{
    if (DateTimeOffset.Now > accessToken.ExpiresOn)
    {
        //call again GetAccessTokenByScopeAsync
    }
    HttpClient httpClient = new HttpClient();
    var defaultRequetHeaders = httpClient.DefaultRequestHeaders;
    if (defaultRequetHeaders.Accept == null || !defaultRequetHeaders.Accept.Any(m => m.MediaType == "application/json"))
    {
        httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    }
    defaultRequetHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Token);
    //see also the app configuration rest api for documentation
    var responseMessage = await httpClient
        .GetAsync("{replace with endpoint (see overview of app configuration)}/kv/?key=%2A&api-version=1.0");
    var outputString = await responseMessage.Content.ReadAsStringAsync();

    using var jDoc = JsonDocument.Parse(outputString);
    outputString = JsonSerializer.Serialize(jDoc, new JsonSerializerOptions { WriteIndented = true });
    Console.WriteLine(outputString);
}

As we did before we need to acquire an access token for the app configuration using the app registered clientid and clientsecret. We use for the scope our app configuration. After retrieving the access token we use it to query the app configuration rest api to access the configuration.

var accessTokenAppConfiguration = await GetAccessTokenByScopeAsync(tenantid, "{replace with endpoint (see overview of app configuration)}/.default", clientid, clientSecret);
await GetAppConfigurationAsync(accessTokenAppConfiguration);

If you don’t want to query the REST api of the App Configuration and use the more connivent Microsoft.Extensions.Configuration.AzureAppConfiguration nuget, the solution would look like the following:

        var configurationBuilder = new ConfigurationBuilder();
        string AppConfigurationConnection = "https://yourentpoint.azconfig.io";
        configurationBuilder.AddAzureAppConfiguration(options =>
            {
                var credential =
                    new ClientSecretCredential(tenantid, clientid, clientSecret);
                options.Connect(new Uri(AppConfigurationConnection), credential);
                options.Select(KeyFilter.Any);
            });
        var configuration = configurationBuilder.Build();

If you fire up Fiddler you will notice that the AzureAppConfiguration will send exactly the same REST api calls as we did before. The first REST call will acquire the access token and the second call will query the app configuration using that token.

Summary

In the above sample we used only the credentials of the registered app to access the Graph Api and App Configuration. When the registered app was set up a service principal was created in the Azure AD which represents our app. We then authorized the service principal to access our App Configuration.

Links: -Why use Managed Identity? -Microsoft identity platform and the OAuth 2.0 client credentials flow