Headless-Daemon calling AAD secured API

In AAD series of articles, we will see how to call the AAD protected secure API from a headless (or) console application and its authentication flows and scenarios.

In the previous article, we have seen how to register an app in aad. I recommend you to read to get familiar with the app registration concept.

How to call the AAD protected secure API from a headless (or) console application

For these types of non-interactive clients, we can use 2 types of authentication flows for different scenarios.

   - client_credentials_grant
   - username/password

The above grant types allow clients to authenticate silently without any user interaction.

oAuth 2.0 client_credentials_grant

Authentication is done based on the valid "Client Secret" used, so this is available only for the "WebApp Type" app registrations. and copy the below information from the respective app registration for later use.

     - ClientID: Client App's AplicationID.
     - ClientSecret: Client App's "Secret key".
     - ResourceID: Consuming endpoint Application Url.

Username/Password

Authentication is done based on user credentials, usually, this option is used for "auth_code_grant" where users will have to enter their credential in an AAD signIn popup in the iFrame scenario. But we can also use it for username/password authentication types.      
     - ClientID: Native Client ApplicationID.

where to find this information? 

log in to https://portal.azure.com 

In this article "CogntiveAPIWebDeamonClients" is my Client app registration for headless-daemon which is counter-intuitive. we need to register the client "WebApp Type" to use the "client_credentials_grant" it uses the secret key to authentication.

In this article "NativeCognitiveHeadlessClientsis also my Client app registration for headless-daemon and register as a "Native Type" application.

In this article "CognitiveAPI" is my custom secure API service endpoint and this is where I can get my ResourceID (i.e. App ID URI).



Check here for detailed registration steps

The code

Create a new Console application and install this NuGet package "Microsoft.IdentityModel.Clients"
Use the copied values as appropriate, to create the following web.config entries
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<!--client_code_grant: Authentication scenario-->
<add key="ida:ClientId" value="c3c3e437-9852-4408-b85b-fba1437d0380" />
<add key="ida:ClientSecret" value="NkxCsNf143QJ+LOhW3Sy/5kZQZAK2F0ChxlThFO/FA=" />
<!--non-interactive: With username/password Authentication scenario-->
<add key="ida:NativeClientId" value="d9071b1b-87c2-4a47-84b9-faf1432d0293" />
<add key="ida:UserName" value="rathanavel@rathanavellive.onmicrosoft.com" />
<add key="ida:Password" value="*******" />
<!--Common_AAD_Keys-->
<add key="ida:Tenant" value="rathanavellive.onmicrosoft.com" />
<add key="ida:AADInstance" value="https://login.microsoftonline.com/{0}" />
<add key="ida:ServiceResourceId" value="https://rathanavellive.onmicrosoft.com/CogntiveAPI" />
<add key="endpoint:ServiceBaseAddress" value="http://ratsubcognitiveapi.azurewebsites.net/" />
</appSettings>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6" />
</startup>
</configuration>
"client_credentials" authenticationClientCredential clientCredential = new ClientCredential(clientId, clientSecret);
Username/Password authentication
UserCredential userCredential = new UserCredential(userName, password);
UserCredential userCredential = new UserCredential();
//Use without parameter if the client runs on same domain in secure environment.


//App Client ID & Secret
private readonly static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
private readonly static string clientSecret = ConfigurationManager.AppSettings["ida:ClientSecret"];
//App Native Client ID
private readonly static string NativeClientId = ConfigurationManager.AppSettings["ida:NativeClientId"];
private readonly static string NativeServicesBaseAddress = ConfigurationManager.AppSettings["ida:NativeServicesBaseAddress"];
//Azure ActiveDirectory details
private readonly static string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
private readonly static string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
//Source service endpoint
private readonly static string serviceResourceId = ConfigurationManager.AppSettings["ida:ServiceResourceId"];
private readonly static string serviceBaseAddress = ConfigurationManager.AppSettings["endpoint:ServiceBaseAddress"];
//Token Issuer authority
private static readonly string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
//Global Variables
private static AuthenticationContext authContext = new AuthenticationContext(authority);
private static ClientCredential clientCredential = new ClientCredential(clientId, clientSecret);
private static AuthenticationResult authResult = null;
static void Main(string[] args)
{
int retryCount = 0;
bool retry = false;
do
{
retry = false;
try
{
#region Using Client Credentials
//synchronous way to aquire token.
authResult = authContext.AcquireToken(serviceResourceId, clientCredential);
//asynchronous way to aquire token.
//Authenticate(serviceResourceId, clientCredential).Wait();
#endregion
#region Using User Credentials
//Provide different username/password to run this application from same/any Network (or) domain.
var userName = ConfigurationManager.AppSettings["ida:UserName"];
var password = ConfigurationManager.AppSettings["ida:Password"];
UserCredential userCredential = new UserCredential(userName, password);
//Use this to run under current LoggedIn user identity, This works for scenario where Desktop/Laptop/VM in connected to same the same domain.
//UserCredential userCredential = new UserCredential();
authResult = authContext.AcquireToken(serviceResourceId, NativeClientId, userCredential);
#endregion
}
catch (AdalException ex)
{
if (ex.ErrorCode == "temporarily_unavailable")
{
Console.WriteLine(ex.ErrorCode + Environment.NewLine + ex.Message);
retry = true;
}
else
{
retry = true;
Console.WriteLine(ex.ErrorCode + Environment.NewLine + ex.Message);
}
Console.WriteLine(string.Format("Retrying ({0}) of 3 attempt(s)", (retryCount + 1).ToString()));
retryCount++;
Thread.Sleep(3000);
}
catch (Exception x)
{
Console.WriteLine(x.Message);
Console.ReadLine();
return;
}
} while ((retry == true) && (retryCount < 3));
if (authResult == null)
{
Console.WriteLine("Cancelling attempt ..");
Thread.Sleep(2000);
return;
}
Console.WriteLine("Authenticated succesfully.." + Environment.NewLine + "Accessing Enpoint.." + Environment.NewLine);
AccessEnpoint(authResult, "api/videos").Wait();
AccessEnpoint(authResult, "api/video/1").Wait();
AccessEnpoint(authResult, "api/video/1/frames").Wait();
Console.ForegroundColor = ConsoleColor.Green;
Console.Write("Completed!");
Console.ReadLine();
return;
}
/// <summary>
/// Helper method to acquire authentication result in async way.
/// </summary>
/// <param name="resource">Resource id</param>
/// <param name="cred">Credentials</param>
/// <returns></returns>
private static async Task Authenticate(string resource, ClientCredential cred)
{
authResult = await authContext.AcquireTokenAsync(serviceResourceId, clientCredential);
}
Finally, use the Bearer token from the AuthenticationResult object to access common secure endpoint
/// <summary>
/// Access secure enpoint using authenticated context Bearer token.
/// </summary>
/// <param name="authResult">AuthenticationResult: Authenticated token object</param>
/// <param name="enpoint">Resource enpoint</param>
/// <returns></returns>
private static async Task AccessEnpoint(AuthenticationResult authResult, string enpoint)
{
HttpClient httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
HttpResponseMessage response = await httpClient.GetAsync(serviceBaseAddress + enpoint);
if (response.IsSuccessStatusCode)
{
string output = await response.Content.ReadAsStringAsync();
Console.WriteLine(output);
}
else
{
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Access Denied!");
authContext.TokenCache.Clear();
}
else
{
Console.WriteLine(response.ReasonPhrase);
}
}
Console.WriteLine(Environment.NewLine + "*******" + Environment.NewLine);
}

Some key points to remember:-
  1. Anyone(or) any authenticated application knows you "Client ID & Secret" can access your API. It's API's responsibility to ensure the current request came from the legitimate client based on ClaimsPrincipal alternatively you can also define custom "Roles" (i.e. Application Permission) & "Scope" (i.e. Delegated Permission) for your API while registering the app in AAD. - How to do this? This will see in the upcoming article.
    • Question is as an Azure Admin how to securely share the "Client ID & Secret" to an external ISV or any developer: I could think of 2 options
    • If you are creating and hosting it has an Azure App Service as a WebJob, then you create an "Application Setting" and share that key to your the developer.
    • Use Azure Key Vault to store and access the key securely, It also provides various security controls/policy/IP range etc.. to expose and access this key.
  2. If you choose to authenticate using "Native" registration type (Username/Password). then the client is on a trusted sub-system (i.e. Desktop/Server/VM/Laptop) where the client runs in the same domain. But you can still run this client from any domain if you know the username/password and update them in app.config. But still, request legitimate check like "Roles" are in API side only.
Hope I have covered all the nuances of non-interactive AAD authentication. in the next article will see how to use in Windows-Native-App calling secure WebAPI. Catch you all there!

-Ratsub

Comments

Post a Comment

Enter your comments..

Popular posts from this blog

Secure When a HTTP request is received Power Automate a.k.a MS Flow

People picker Control in PowerApps

Upload attachment to SharePoint list item using Microsoft Flow

Modern page provisioning using page template

Approval and auto escalation with time out in Microsoft Flow

Developing custom reusable components in PowerApps

Step-By-Step Azure AD App Registration

Create and configure custom connectors for PowerApps and MSFlow from AzureFunctions

HTML field & Date Time formatting in powerapps