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.
- client_credentials_grant
- username/password
The above grant types allow clients to authenticate silently without any user interaction.
- ClientID: Client App's AplicationID.
- ClientSecret: Client App's "Secret key".
- ResourceID: Consuming endpoint Application Url.
- ClientID: Native Client ApplicationID.
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 "NativeCognitiveHeadlessClients" is 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
Use the copied values as appropriate, to create the following web.config entries
"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.
Finally, use the Bearer token from the AuthenticationResult object to access common secure endpoint
Some key points to remember:-
-Ratsub
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.comIn 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 "NativeCognitiveHeadlessClients" is 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).

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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
Username/Password authentication
UserCredential userCredential = new UserCredential();
//Use without parameter if the client runs on same domain in secure environment.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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:-
- 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.
- 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!
Nice Article thank you for posting this valuable information.
ReplyDelete