Create an Azure App registrations daemon app
By Martin 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
In Azure go to Home and select your Azure Active Directory. Select
App registration
: Home > Default Directory | App registrationsSelect
New registration
Enter a name (for example
azureappreg
) and selectAccounts in this organizational directory only (Default Directory only - Single tenant)
You should now see a new page: Home > Default Directory | azureappreg
In the left menu select:
Expose an Api
and under the title “Scopes defined by this API” pressAdd a scope
Enter an Application Scope. Instead of the guid you can enter
api://azureappregScope
Next enter Scope name (for example
azureappreg.Read
), a consent display name for exampleRead
and a consent description. Finish withAdd Scope
.In the left menu select
Certificates & secrets
. SelectClient secrets
. SelectNew 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.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 withUser.Read
withDelegated
was automatically added when we created the App registration. BecauseDelegated
(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.Select
Add Permission
. SelectMicrosoft Graph
andApplication permissions
. Under Permissions select (Application)Application.Read.All
and (User)User.Read.All
. PressAdd Permission
.Press the button
Grant admin consent
and confirm withYes
.Its time to test our setup. Go to the left menu and select
Overview
. Obtain the Guid values fromApplication (client) ID
,Directory (tenant) ID
andApplication 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;
.WriteLine("Hello, World!");
Console
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);
GetApplicationsByGraphApiAsync(accessToken);
await
<AccessToken> GetAccessTokenByScopeAsync(string tenantid, string scope, string clientId, string registeredAppSecrect)
async Task{
//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")
});
= new HttpClient();
HttpClient 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));
}
GetApplicationsByGraphApiAsync(AccessToken accessToken)
async Task {
if (DateTimeOffset.Now > accessToken.ExpiresOn)
{
//call again GetAccessTokenByScopeAsync
}
= new HttpClient();
HttpClient httpClient var defaultRequetHeaders = httpClient.DefaultRequestHeaders;
if (defaultRequetHeaders.Accept == null || !defaultRequetHeaders.Accept.Any(m => m.MediaType == "application/json"))
{
.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpClient}
.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Token);
defaultRequetHeadersvar responseMessage = await httpClient.GetAsync($"https://graph.microsoft.com/v1.0/applications/");
var outputString = await responseMessage.Content.ReadAsStringAsync();
using var jDoc = JsonDocument.Parse(outputString);
= JsonSerializer.Serialize(jDoc, new JsonSerializerOptions { WriteIndented = true });
outputString .WriteLine(outputString);
Console
= await httpClient.GetAsync($"https://graph.microsoft.com/v1.0/users/");
responseMessage = await responseMessage.Content.ReadAsStringAsync();
outputString .WriteLine(outputString);
Console//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:
- 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). - In the left menu of the App Configuration open Access Control (IAM).
Select
Add role assignment
under “Grant access to this resource”. - Choose for Role
App Configuration Data Reader
. On the next tabMembers
selectUser, group, or service principal
and press “+Select Members” and search for the name of registered app (in this sample the name wasazureappreg
you will find the name also in the overview section of the registered app). To complete the operation press the buttonReview + assign
. - Go to the overview of the App Configuration and copy the endpoint.
In the previous console project add the following method.
GetAppConfigurationAsync(AccessToken accessToken)
async Task {
if (DateTimeOffset.Now > accessToken.ExpiresOn)
{
//call again GetAccessTokenByScopeAsync
}
= new HttpClient();
HttpClient httpClient var defaultRequetHeaders = httpClient.DefaultRequestHeaders;
if (defaultRequetHeaders.Accept == null || !defaultRequetHeaders.Accept.Any(m => m.MediaType == "application/json"))
{
.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpClient}
.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Token);
defaultRequetHeaders//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);
= JsonSerializer.Serialize(jDoc, new JsonSerializerOptions { WriteIndented = true });
outputString .WriteLine(outputString);
Console}
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);
GetAppConfigurationAsync(accessTokenAppConfiguration); await
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";
.AddAzureAppConfiguration(options =>
configurationBuilder{
var credential =
new ClientSecretCredential(tenantid, clientid, clientSecret);
.Connect(new Uri(AppConfigurationConnection), credential);
options.Select(KeyFilter.Any);
options});
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
- Quickstart: Acquire a token and call the Microsoft Graph API by using a console app’s identity
- Protected web API: App registration
- Managing applications using Azure AD, service principals and managed identities: A permissions story
- Get access token from custom API using Azure CLI or PowerShell
- Application permissions is greyed out after exposing an API in Azure AD
- Introduction to Azure App Configuration for Developers with C# .NET Core