I'm currently in the process of converting my Power BI embedded application from .NET to .NET core.
My old code for generating tokens looked something like this:
var credential = new UserPasswordCredential(Username, Password);
var authContext = new AuthenticationContext(AuthorityUrl);
var authResult = authContext.AcquireTokenSilentAsync(ResourceUrl, _applicationId).Result;
However, by design .NET Core does not support UserPasswordCredential.
Gunnar Peipman in a recent article "Embedded Power BI reports with ASP.NET Core" used a HTTP request to solve this, but is this the recommended approach?
private async Task<string> GetPowerBIAccessToken(PowerBISettings powerBISettings)
{
using(var client = new HttpClient())
{
var form = new Dictionary<string, string>();
form["grant_type"] = "password";
form["resource"] = powerBISettings.ResourceUrl;
form["username"] = powerBISettings.UserName;
form["password"] = powerBISettings.Password;
form["client_id"] = powerBISettings.ApplicationId.ToString();
form["client_secret"] = powerBISettings.ApplicationSecret;
form["scope"] = "openid";
client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/x-www-form-urlencoded");
using (var formContent = new FormUrlEncodedContent(form))
using (var response = await client.PostAsync(powerBISettings.AuthorityUrl, formContent))
{
var body = await response.Content.ReadAsStringAsync();
var jsonBody = JObject.Parse(body);
var errorToken = jsonBody.SelectToken("error");
if(errorToken != null)
{
throw new Exception(errorToken.Value<string>());
}
return jsonBody.SelectToken("access_token").Value<string>();
}
}
}
No, the recommended approach is to use ClientCredential class.
The idea here is that for apps to collect and store a user's username and password is not the correct approach for non-interactive authentication.
Also see Microsoft identity platform and the OAuth 2.0 client credentials flow and Acquiring Tokens:
Confidential client applications the flows will rather:
Acquire token for the application itself, not for a user, using client credentials.
And one more quote:
Pattern to acquire tokens in MSAL 3.x
All the Acquire Token methods in MSAL 3.x have the following pattern:
from the application, you call the AcquireTokenXXX method corresponding to the flow you want to use, passing the mandatory parameters for this flow (in general flow)
this returns a command builder, on which you can add optional parameters using .WithYYY methods
then you call ExecuteAsync() to get your authentication result.
Here is the pattern:
AuthenticationResult result = app.AcquireTokenXXX(mandatory-parameters)
.WithYYYParameter(optional-parameter)
.ExecuteAsync();
An example (again by Gunnar) how to do this:
public async Task<AuthenticationResult> RequestTokenAsync(
ClaimsPrincipal claimsPrincipal,
string authorizationCode,
string redirectUri,
string resource)
{
try
{
var userId = claimsPrincipal.GetObjectIdentifierValue();
var issuerValue = claimsPrincipal.GetIssuerValue();
var authenticationContext = await CreateAuthenticationContext(claimsPrincipal)
.ConfigureAwait(false);
var authenticationResult = await authenticationContext.AcquireTokenByAuthorizationCodeAsync(
authorizationCode,
new Uri(redirectUri),
new ClientCredential(_adOptions.ClientId, _adOptions.ClientSecret),
resource)
.ConfigureAwait(false);
return authenticationResult;
}
catch (Exception)
{
throw;
}
}
Related
I'm trying to call an Azure Machine Learning Pipeline Endpoint I've set up using C# & the Machine Learning REST api.
I am certain that I have the Service Principal configured correctly, as I can successfully authenticate & hit the endpoint using the azureml-core python sdk:
sp = ServicePrincipalAuthentication(
tenant_id=tenant_id,
service_principal_id=service_principal_id,
service_principal_password=service_principal_password)
ws =Workspace.get(
name=workspace_name,
resource_group=resource_group,
subscription_id=subscription_id,
auth=sp)
endpoint = PipelineEndpoint.get(ws, name='MyEndpoint')
endpoint.submit('Test_Experiment')
I'm using the following example in C# to attempt to run my endpoint: https://learn.microsoft.com/en-us/azure/machine-learning/how-to-deploy-pipelines#run-a-published-pipeline-using-c
I'm attempting to fill auth_key with the following code:
var clientId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID");
var clientSecret = Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET");
var tenantId = Environment.GetEnvironmentVariable("AZURE_TENANT_ID");
var cred = new ClientSecretCredential(tenantId, clientId, clientSecret);
var auth_key = cred.GetToken(new Azure.Core.TokenRequestContext(new string[] {".default" }));
I receive a 401 (unauthorized).
What am I am doing wrong?
UPDATE *
I changed the 'scopes' param in the TokenRequestContext to look like:
var auth_key = cred.GetToken(new Azure.Core.TokenRequestContext(new string[] { "http://DataTriggerApp/.default" }));
http://DataTriggerApp is one of the servicePrincipalNames that shows up when i query my Service Principal from the azure CLI.
Now, when I attempt to use the returned token to call the Machine Learning Pipeline Endpoint, I receive a 403 instead of a 401. Maybe some progress?
Ok, through a lot of trial-and-error I was able to come up with two ways of acquiring a token that allows me to hit my Azure Machine Learning Pipeline Endpoint through the REST api. One uses Microsoft.Identity.Client & one uses Azure.Identity.
using Microsoft.Identity.Client;
...
public static async Task<string> GetAccessToken()
{
var clientId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID");
var clientSecret = Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET");
var tenantId = Environment.GetEnvironmentVariable("AZURE_TENANT_ID");
var app = ConfidentialClientApplicationBuilder.Create(clientId)
.WithClientSecret(clientSecret)
.WithAuthority(AzureCloudInstance.AzurePublic, tenantId)
.Build();
var result = await app.AcquireTokenForClient(new string[] { "https://ml.azure.com/.default" }).ExecuteAsync();
return result.AccessToken;
}
Or:
using Azure.Identity;
...
public static async Task<string> GetAccessToken()
{
var clientId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID");
var clientSecret = Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET");
var tenantId = Environment.GetEnvironmentVariable("AZURE_TENANT_ID");
var cred = new ClientSecretCredential(tenantId, clientId, clientSecret);
var token = await cred.GetTokenAsync(new Azure.Core.TokenRequestContext(new string[] { "https://ml.azure.com/.default" }));
return token.Token;
}
I have tried other answers before I post this question.
I have created a web API and published into azure web app. I have enabled azure AD authentication. I have created one app registration and given permissions to AD app. In my console application, I am calling as below:
private static async Task<string> GetToken()
{
string aadInstance = "https://login.windows.net/common";
string ResourceId = ConfigurationManager.AppSettings["ResourceId"];
string tenantId = ConfigurationManager.AppSettings["TenantId"];
string clientId = ConfigurationManager.AppSettings["ClientId"];
string replyAddress = ConfigurationManager.AppSettings["ReplyAddressConfigured"];
string password = ConfigurationManager.AppSettings["ida:Password"];
AuthenticationContext authenticationContext =
new AuthenticationContext(string.Format(aadInstance, tenantId));
PlatformParameters k = new PlatformParameters(PromptBehavior.Auto);
AuthenticationResult authenticationResult = await authenticationContext.AcquireTokenAsync(ResourceId, clientId, new Uri(replyAddress),k).ConfigureAwait(false);
return authenticationResult.AccessToken;
}
static async Task Main(string[] args)
{
var accessToken = await GetToken();
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var responseTask = client.GetAsync("https://myapi.azurewebsites.net/api/GetData/GetLicensing?appName=test&tenantId=f556301-8a1e-48c3-b59e-55a9036f843e");
responseTask.Wait();
var result = responseTask.Result;
if (result.IsSuccessStatusCode)
{
var readTask = result.Content.ReadAsStringAsync();
readTask.Wait();
}
}
}
IsSuccessStatusCode code is coming as false with 401. What is the mistake here. But if I use client and secret it is successful. It is not possible to call without passing client secret value?
Below is just illustration:
You cannot get correct token without client secret if you are calling the API without using MSI.
But, if you use MSI, in both API and hosted Console app on cloud, you can call API from the Console app without Client secret.
My goal is to protect a Web API, such that it can only be accessed by a client using an access token issued by IS based on Windows authentication. I worked through this basic sample:
http://docs.identityserver.io/en/release/quickstarts/1_client_credentials.html
Now, I need to extend the basic sample such that the access token returned to the client is issued based on Windows authentication. More specifically, I need to have the user (which is executing the client application) to be authenticated against Active Directory when requesting an access token. How should this be done?
I have already been running the quick start (https://github.com/IdentityServer/IdentityServer4.Templates) successfully, where the login is based on a Windows external provider, but I cannot figure out how to adopt this functionality to my strategy.
I tried using an Extension Grant (http://docs.identityserver.io/en/release/topics/extension_grants.html) and have the ValidateAsync() method be the one to do the authentication against AD, but could not make it work (primarily since HttpContext is not available). Is this even the correct approach?
Update
In this system, the client is a console application (without human interaction), thus the context is the account running the application.
I have been running the QuickstartUI and see how the AccountController logic handles the "Windows" button, but cannot grasp how to combine this with requesting access tokens. My client code goes like this:
static async Task Main(string[] args)
{
var disco = await DiscoveryClient.GetAsync("http://localhost:50010");
var tokenClient = new TokenClient(disco.TokenEndpoint);
var tokenResponse = await tokenClient.RequestCustomGrantAsync("CustomWindows"); // Not sure about this
var client = new HttpClient();
client.SetBearerToken(tokenResponse.AccessToken);
var response = await client.GetAsync("http://localhost:50011/api/identity");
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(JArray.Parse(content));
Console.ReadLine();
}
I am not sure how to use the TokenClient to get an access token in this case. I would prefer not to store and use passwords, but have IS issue access tokens based on authenciating the client context against AD. If implicit or hybrid flows must be used in this case, how must that be done?
I had the same requirement and implemented it using an extension grant.
This is the code of the extension grant:
public class WinAuthGrantValidator : IExtensionGrantValidator
{
private readonly HttpContext httpContext;
public string GrantType => WinAuthConstants.GrantType;
public WinAuthGrantValidator(IHttpContextAccessor httpContextAccessor)
{
httpContext = httpContextAccessor.HttpContext;
}
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
// see if windows auth has already been requested and succeeded
var result = await httpContext.AuthenticateAsync(WinAuthConstants.WindowsAuthenticationSchemeName);
if (result?.Principal is WindowsPrincipal wp)
{
context.Result = new GrantValidationResult(wp.Identity.Name, GrantType, wp.Claims);
}
else
{
// trigger windows auth
await httpContext.ChallengeAsync(WinAuthConstants.WindowsAuthenticationSchemeName);
context.Result = new GrantValidationResult { IsError = false, Error = null, Subject = null };
}
}
}
And this is the client code:
var httpHandler = new HttpClientHandler
{
UseDefaultCredentials = true,
};
// request token
var tokenClient = new TokenClient(disco.TokenEndpoint, "client", "secret", httpHandler, AuthenticationStyle.PostValues);
var tokenResponse = await tokenClient.RequestCustomGrantAsync("windows_auth", "api1");
So I believe my APIservice should be fine since I can return results through Swagger? I am calling from a WPF project. I launch the program and it asks me to login, then it continues and will tell me I don't have permission.
I'm super green to WebAPI2 and think I may just be constructing my call incorrectly. It does seem that I get a token back correctly from my site, the only issue is when I try to actually call on the API for data.
Here is my code:
public static string clientId = "{#Calling App Id}";
public static string commonAuthority = "https://login.windows.net/{#my Azure AD tenant}";
public static Uri returnUri = new Uri("http://MyDirectorySearcherApp");
const string ResourceUri = "https://{#Api App Service}.azurewebsites.net";
public static async Task<List<User>> LoadBands(IPlatformParameters parent)
{
AuthenticationResult authResult = null;
List<User> results = new List<User>();
try {
//get token or use refresh
AuthenticationContext authContext = new AuthenticationContext(commonAuthority);
if (authContext.TokenCache.ReadItems().Count() > 0)
authContext = new AuthenticationContext(authContext.TokenCache.ReadItems().First().Authority);
authResult = await authContext.AcquireTokenAsync(ResourceUri, clientId, returnUri, parent);
} catch (Exception ee) {
throw ex;
}
using (var httpClient = new HttpClient()) {
using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, $"{ResourceUri}/api/Band/")) {
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
using (var response = await httpClient.SendAsync(request)) {
string responseData = await response.Content.ReadAsStringAsync();
//responseData always equals "You do not have permission to view this directory or page"
return results;
}
}
}
Edit: Maybe helpful to note I'm using a DataAPI that is called by a Rest API, the rest API is secured by Azure AD.
Edit: I'm calling from a Portable Class Library.
Edit: Well, I'm getting authenticated but it does not appear to make any difference. If I completely remove the Auth header I get the same result
It seems that the token is incorrect for the web API which protected by Azure AD. Please check the aud claim in the token which should match the Audience you config in the web API project. You can check the aud claim by parse the token from this site.
And if you still have the problem please share the code how you protect the web API.
Update
If you were using the Express mode like below, you need to acquire the access_token using the app which you associate with the web API.
If you were using the Advanced mode, we should also use the that app to acquire the token and the ResourceUri should matched the value you config in ALLOWED TOKEN AUDIENCES like below:
I would like to be programmatically able to get a token from Azure.
I call GetAToken().Wait(); and it fails.
and the method is:
public async Task<string> GetAToken()
{
// authentication parameters
string clientID = "*********";
string username = "<azure login>";
string password = "<azure login password>";
string directoryName = "<AD Domain name>";
ClientCredential cc = new ClientCredential(clientID, password);
var authenticationContext = new AuthenticationContext(
"https://login.windows.net/" + directoryName);
AuthenticationResult result = await authenticationContext.AcquireTokenAsync(
"https://management.core.windows.net/", cc);
if (result == null)
{
throw new InvalidOperationException("Failed to obtain the JWT token");
}
string token = result.AccessToken;
return token;
}
So not sure if you are doing this on Android, iOS or Xamarin.Forms. Below is how I will authenticate with ADAL and Azure (the code is working on my end):
On Android:
public async Task<AuthenticationResult> Authenticate(Activity context, string authority, string resource, string clientId, string returnUri)
{
var authContext = new AuthenticationContext(authority);
if (authContext.TokenCache.ReadItems().Any())
authContext = new AuthenticationContext(authContext.TokenCache.ReadItems().First().Authority);
var uri = new Uri(returnUri);
var platformParams = new PlatformParameters(context);
try
{
var authResult = await authContext.AcquireTokenAsync(resource, clientId, uri, platformParams);
return authResult;
}
catch (AdalException e)
{
return null;
}
}
On iOS:
public async Task<AuthenticationResult> Authenticate(UIViewController controller, string authority, string resource, string clientId, string returnUri)
{
var authContext = new AuthenticationContext(authority);
if (authContext.TokenCache.ReadItems().Any())
authContext = new AuthenticationContext(authContext.TokenCache.ReadItems().First().Authority);
var controller = UIApplication.SharedApplication.KeyWindow.RootViewController;
var uri = new Uri(returnUri);
var platformParams = new PlatformParameters(controller);
try
{
var authResult = await authContext.AcquireTokenAsync(resource, clientId, uri, platformParams);
return authResult;
}
catch (AdalException e)
{
return null;
}
}
On UWP:
public async Task<AuthenticationResult> Authenticate(string authority, string resource, string clientId, string returnUri)
{
var authContext = new AuthenticationContext(authority);
if (authContext.TokenCache.ReadItems().Any())
authContext = new AuthenticationContext(authContext.TokenCache.ReadItems().First().Authority);
var uri = new Uri(returnUri);
var platformParams = new PlatformParameters(PromptBehavior.Auto);
try
{
var authResult = await authContext.AcquireTokenAsync(resource, clientId, uri, platformParams);
return authResult;
}
catch (AdalException e)
{
return null;
}
}
Variable that I pass into the methods above:
string authority = "https://login.windows.net/common";
string ResourceID = "Backend ClientId";//Backend (web app)
string clientId = "Native App ClientId";//native app
string returnUri = "https://{My Azure Site}.azurewebsites.net/.auth/login/done";
If you want to do this in Xamarin.Forms, below are links to my GitHub solution where I have exposed these methods through the DependencyService.
PCL implementation
iOS implementation
Android Implementation
I hope this helps! If you get any errors from your response, check to make sure you have your permissions setup in Azure correctly. I do it like this. Another great resource is Adrian Hall's Xamarin/Azure book
EDIT: Added UWP stuff
If what you are trying to do is call the Azure APIs as you, there are a few things you should do differently.
Create an app in Azure AD that has permissions to access the Azure API
If you want to call Service Management API, then add that as a permission
You could also alternatively use a management certificate
If you want to call Resource Management API, then add the permissions needed to the service principal through the new Portal
If you chose the delegated way for Service Management API (the first option), then you will have to either:
Have the user authenticate against Azure AD with the Authorization Code Grant flow
Or get the access token using the Password grant flow (you can see an example of this in another answer
If instead you chose a management certificate or giving the permissions to the service principal, then you can get the access token directly from Azure AD using the Client credentials grant flow
In the end you will always end up with an access token that you can use for calling the API.
IF you're using the wrappers, ensure to have the correct version-Microsoft.IdentityModel.Clients.ActiveDirectory -Version 2.21.301221612.
Once referenced, you can run this below. For alternatives, see this blog: https://samtran.me/2018/11/11/power-bi-rest-api/
If you are also running into issue on Android where device rotation returns you back to prompt for user email, you can follow up progress of fixes for both ADAL and MSAL here:
https://github.com/AzureAD/azure-activedirectory-library-for-dotnet/issues/1622
https://github.com/xamarin/xamarin-android/issues/3326