Can't Authenticate Power Query to ASP.NET Core MVC Web App - authentication

Bottom line up front... when I try to connect Power Query from Excel to my app, I get the error described here - We were unable to connect because this credential type isn’t supported for this resource. Please choose another credential type.
Details...
My app was created from the ASP.NET Core Web App MVC template.
It uses the Microsoft Identity Platform for authentication.
// Sign-in users with the Microsoft identity platform
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
builder.Configuration.Bind("AzureAd", options);
options.Events = new OpenIdConnectEvents();
options.Events.OnTokenValidated = async context =>
{
//Calls method to process groups overage claim.
var overageGroupClaims = await GraphHelper.GetSignedInUsersGroups(context);
};
})
.EnableTokenAcquisitionToCallDownstreamApi(options => builder.Configuration.Bind("AzureAd", options), initialScopes)
.AddInMemoryTokenCaches();
It uses Azure AD group claims for authorization.
// Adding authorization policies that enforce authorization using group values.
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Administrators", policy => policy.Requirements.Add(new GroupPolicyRequirement(builder.Configuration["Groups:Administrators"])));
options.AddPolicy("SuperUsers", policy => policy.Requirements.Add(new GroupPolicyRequirement(builder.Configuration["Groups:SuperUsers"])));
options.AddPolicy("Deliverables", policy => policy.Requirements.Add(new GroupPolicyRequirement(builder.Configuration["Groups:Deliverables"])));
options.AddPolicy("Orders", policy => policy.Requirements.Add(new GroupPolicyRequirement(builder.Configuration["Groups:Orders"])));
options.AddPolicy("Users", policy => policy.Requirements.Add(new GroupPolicyRequirement(builder.Configuration["Groups:Users"])));
});
builder.Services.AddSingleton<IAuthorizationHandler, GroupPolicyHandler>();
In addition to the standard MVC UI, it also has an OData controller for API access.
[ApiController]
[Route("[controller]")]
//[Authorize(Policy = "Users")]
[AllowAnonymous]
public class OdataController : Controller Base
From a browser, everything works perfectly. Access is properly controlled by [Authorize(Policy = "Users")].
From Excel, however, it only works if I set the controller to [AllowAnonymous] and use the Get Data > From Web option.
If I try Get Data > From OData Feed, I get an error The given URL neither points to an OData service or a feed. Which is OK as long as Get Data > From Web works.
If I add the authorization policy, I get an error We were unable to connect because this credential type isn’t supported for this resource. Please choose another credential type.
Following the supported workflow here, Power Query is expecting a 401 response with a WWW_Authentication header containing the Azure AD login URL. Instead, it's being sent directly to the Azure login, and therefore the authentication fails. I did try adding the Power Query client IDs to my Azure AD app, but that had no effect.
I have done every search I can think of and am out of ideas. Can anyone help? Thanks!

Related

How to configure Azure AD authentication in Hybrid ASP.NET Core MVC (backend) and Vuejs SPA (frontend)?

My application is a hybrid approach where use ASP.NET Core MVC as my backend. I have various controllers which my front end uses to pull data from our database and also to do API calls on MS Graph. I am using the following program.cs file to get the authentication initiated when a user first logs on to the site:
//authentication pipline
builder.Services.AddHttpContextAccessor();
var initialScopes = builder.Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
builder.Configuration.Bind("AzureAd", options);
options.Events = new OpenIdConnectEvents
{
//Tap into this event to add a UserID Claim to a new HttpContext identity
OnTokenValidated = context =>
{
//This query returns the UserID from the DB by sending the email address in the claim from Azure AD
string query = "select dbo.A2F_0013_ReturnUserIDForEmail(#Email) as UserID";
string connectionString = builder.Configuration.GetValue<string>("ConnectionStrings:DBContext");
string signInEmailAddress = context.Principal.FindFirstValue("preferred_username");
using (var connection = new SqlConnection(connectionString))
{
var queryResult = connection.QueryFirst(query, new { Email = signInEmailAddress });
var claims = new List<Claim>
{
new Claim("UserID", queryResult.UserID.ToString())
};
var appIdentity = new ClaimsIdentity(claims);
context.Principal.AddIdentity(appIdentity);
}
return Task.CompletedTask;
},
};
}).EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
//Add Transient Services
builder.Services.AddTransient<IOneDrive, OneDrive>();
builder.Services.AddControllers(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
builder.Services.AddRazorPages().AddRazorPagesOptions(options =>
{
options.Conventions.AllowAnonymousToFolder("/Login");
options.Conventions.AuthorizeFolder("/");
options.Conventions.AuthorizeFolder("/files");
}).AddMicrosoftIdentityUI();
// Add the UI support to handle claims challenges
builder.Services.AddServerSideBlazor()
.AddMicrosoftIdentityConsentHandler();
builder.Services.AddRequiredScopeAuthorization();
In the Azure AD portal my application is registered as a web app. So when a user initially goes to the site they are redirected to https://login.microsoftonline.com/blahblah to get the login process started. This is automated by the Azure AD identity platform. Then once the login occurs they are redirected to localhost where the VueJS spa is loaded (localhost:43862). My spa uses various axios requests to the controllers and they pull data and vue router loads components. However, my issue is say the user needs to relog in because the cookie is expired or they logged out in another tab. The next axios request made by the expired session does not redirect the user to Azure login screen but instead results in an CORS error. So I need to get my axios requests to force the page redirect to Azure AD login screen (which probably is the worst idea since CORS policy is resulting in error) or have it return a redirect to localhost/login which is my own custom login screen with a button to Azure AD login and shouldnt impact CORS. So how do I intercept this Azure AD redirect to Azure AD login and replace with my own?
I have also tried to return a 401 error code so I could check for that in my axios request but to no avail it does nothing. If I put a breakpoint there it does hit this code but it does not change the status code of the response and I still get 302. My code for that was to try and add to the event :
OnRedirectToIdentityProvider = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
}
My other ideas was maybe I should set my CORS policy to allow redirects from login.microsoft.com? Or would this be bad practice?
I can answer part of your question... First, for our API application which is protected by Azure AD, what the API should do is validating the request whether it contained a correct access token in the request header, if yes, give the response, if no, then give error like 401 or 403. A normal API application shouldn't have a UI to let users sign in. Anyway, if you want to expose an API in an MVC project, it's OK, but for API itself, it shouldn't have a UI.
Let's see sample below, I had a .net 6 web api project, and here's my program.cs:
using Microsoft.Identity.Web;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration);
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
And it requires configurations in appsetting.json.
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"ClientId": "azure_ad_client_id",
"ClientSecret": "client_secret",
"Domain": "tenant_id",
"TenantId": "tenant_id",
//"Audience": "api://azure_ad_client_id_which exposed_api" // here I used the same azure ad app to expose API, so I can comment this property
},
And this is the Controller:
[ApiController]
[Route("[controller]")]
[Authorize]
public class WeatherForecastController : ControllerBase
{
[RequiredScope("Tiny.Read")]
[HttpGet]
public string Get()
{
return "world";
}
}
I had an Azure AD app, and I exposed an API like this:
I also add this API for the same Azure AD app.
Then let's do a test. When I call this API directly, I will get 401 error:
If I used an expired token within the request, I will also get 401 error:
But if I used a correct token(go to https://jwt.io to decode the token, we should see it containing correct scope, for me its "scp": "Tiny.Read",), I will get response:
And till now, the API part had finished. Let's see the client SPA. For SPA, you should integrate MSAL so that you can make your users to sign in via Azure AD, and generate the access token for calling MS graph API or your own API. The code for generating access token should be the same but you should set different scope for different API. In my scenario, my API required a scope Tiny.Read, then I should set in my client App.
Here's an screenshot for generating access token in react. You need to set the scope in your code.
Now you have the method to generate access token, you already know the API url. Then you can send request to call api, using AJAX, using fetch, or something else, sending an http request is ok. And in the calling api part, you also need to handle the response. If the response code is 401, then you need to do some logic, maybe redirect to the sign in page. And you said you had trouble here, you met CORS issue. I can't answer this part. I think it depends on how you redirect to Azure AD sign in page. I'm afraid you can take a look at this sample to learn how to sign in users and call graph api.

Dynamic Authority value for multi-tenant application in .NET 5

I'm working with a multi-tenant application where we have an API project and another WEB project (razor pages). In this context, each customer (tenant) has a specific database and there is a third project, called Manager which is responsible for storing the information of each tenant. Thus, whenever a client accesses one of the projects (WEB or API) we identify the database and the parameters needed by the tenant based on their domain. For example:
client1-api.mydomain.com
client1-web.mydomain.com
client2-api.mydomain.com
client2-web.mydomain.com
Because the WEB and API projects are unique and have multiple databases (one for each tenant), Identity Server 4 was configured in the WEB project, so that whenever a client needs to connect, it logs in via the WEB project or generate a JWT token via the address {clientname}-web.mydomain.com/connect/token (for example: client1-web.mydomain.com/connect/token).
Thus, the API has an Identity Server 4 authentication and authorization configuration, so that the token generated in the WEB project can be used to access its resources.
However, we are not able to make the Authority parameter dynamically update on a per-customer basis (the tenant). The initial idea was to build a middlaware that checks the connected tenant and loads, through the Manager, the Authority parameter and set it whenever a request is made. In general, middleware works and the Authority parameter is set. However, whenever a client tries to access an API resource, the token is invalid and therefore the request is not authorized. Analyzing this problem, I verified that whenever the API receives a request, it tries to validate the token in the WEB project, but when analyzing the address of the Tenant that is making the request to the WEB project, I noticed that it is the same that is set by default in the method ConfigureServices and with that, authentication and authorization only works for this default client.
So is there any way to dynamically set the Identity Server 4 Authority in an application with this scenario?
API Startup:
public void ConfigureServices(IServiceCollection services)
{
//other settings
services.AddAuthentication(option =>
{
option.DefaultAuthenticateScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;
option.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddIdentityServerAuthentication(options =>
{
options.Authority = "https://client1-web.mydomain.com";
});
services.AddAuthorization(options =>
{
options.AddPolicy("ApiScope", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "any1", "any2");
});
});
//other settings
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
//other settings
app.UseAuthentication();
app.UseAuthorization();
app.UseTenantConfig();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers()
.RequireAuthorization("ApiScope");
});
//other settings
}
TENANT PARAMETER CONFIGURATION MIDDLAWARE
public async Task Invoke(HttpContext context,
ITenantParameterService tenantParameterService,
IOptions<IdentityServerAuthenticationOptions> identityServerAuthenticationOptions)
{
var tenantInfo = context.RequestServices.GetRequiredService<TenantInfo>(); //Get tenant domain
var connectionString = tenantParameterService.GetConnectionStringByTenant(tenantInfo.Address);
var authorityParameter = tenantParameterService.GetParameterByTenant(tenantInfo.Address, "Authority");
var myContext = context.RequestServices.GetService<MyDbContext>();
myContext?.Database.SetConnectionString(connectionString);
identityServerAuthenticationOptions.Value.Authority = authorityParameter;
await _next(context);
}
By executing the code, whenever a request is made in the application, middlaware will update the necessary parameters, including the connection string. However the problem is: if the token is generated in client1-web.mydomain.com to be used in client1-api.mydomain.com the authentication works perfectly due to the default configuration at startup (we use this configuration due to the boostrap of the application when it starts and even leaving null for it to be set dynamically, the authentication problem persists). Now, if the token is generated in client2-web.mydomain.com to be used in client2-api.mydomain.com I get the message saying that the token is invalid: Bearer error="invalid_token", error_description="The issuer 'https://client2-web.mydomain.com' is invalid". When decoding the token generated in this case, it was possible to notice that it is correct, because the iss is like https://client2-web.mydomain.com and therefore, the error is in the token and yes in its validation by the API.

Authorization Role/Policy Attributes Not Working In .Net Core 3

I've had no luck getting any Role or Policy attributes working in .Net Core 3. I started my project with the .Net Core Angular starter project with authentication.
I figured this was something to do with the new .AddDefault methods so I have simplified it as much as I possibly can and it still doesn't work.
Here is my policy:
services.AddAuthorization(options =>
{
options.AddPolicy("IsAdmin", policy =>
policy.RequireClaim("role", "admin"));
});
Here is my controller:
[Authorize(Policy = "IsAdmin")]
[Route("api/[controller]")]
public class AdminController : Controller
{
...
I made a custom Profile service that adds the claim to the token,
var claims = new List<Claim>();
if (await _userManager.IsInRoleAsync(user, "Admin"))
{
claims.Add(new Claim(JwtClaimTypes.Role, "admin"));
}
context.IssuedClaims.AddRange(claims);
Inside my access token (from jwt.io):
Other parts of configure services:
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
...
services.AddAuthentication()
.AddIdentityServerJwt();
The plain [Authorize] tag is working fine with the access token on other controllers.
When I hit this controller with the access token I get a 403 response
What am I missing that is preventing this from working?
I try your code and find that the role claim key has been transformed to the standard Role ClaimsType : http://schemas.microsoft.com/ws/2008/06/identity/claims/role
So using ClaimTypes.Role will fix the problem:
services.AddAuthorization(options => {
options.AddPolicy("IsAdmin", policy =>
{
policy.RequireClaim(ClaimTypes.Role,"admin");
});
});
Demo
You should also be able to achieve this without needing a policy. ASP.NET automatically maps common claims to the Microsoft schema.
When you inspect your access token. You will see you are sending the role claim. But when you look at the claims in the controller, you will notice that it has been transformed to http://schemas.microsoft.com/ws/2008/06/identity/claims/role.
There are two things you can do. Either set the RoleClaimType to ClaimTypes.Role. Like so:
services.Configure<JwtBearerOptions>(IdentityServerJwtConstants.IdentityServerJwtBearerScheme, options => {
options.TokenValidationParameters.RoleClaimType = ClaimTypes.Role;
});
Or tell the JwtSecurityTokenHandler not to map default inbound claims like this:
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
Since it's a static property this can be done at any time. But I set it somewhere during my service registrations.

Identity Server 4 Client Credentials for custom endpoint on token Server

I implemented a token server using Identity Server 4.
I added a custom API endpoint to the token server and struggle with the authentication. The custom endpoint is inherited from ControllerBase and has 3 methods (GET, POST, DELETE).
I intend to call the custom endpoint from within another API using a dedicated client with credentials (server to server) implemented as HttpClient in .NET Core. There is no user involved into this.
For getting the access token I use the IdentityModel DiscoveryClient and TokenEndpoint.
So in sum I did the following so far:
setup "regular" identity server and validate it works -> it works
implement custom endpoint and test it without authorizatio -> it works
add another api resource ("api.auth") with a custom scope "api.auth.endpoint1"
setup a client with client credentials allowing access to scope "api.auth.endpoint1".
implement the HttpClient and test setup -> I get an access token via the Identity Model Token Endpoint.
Now, when I call the endpoint using the HttpClient with the access token I received I get response code 200 (OK) but the content is the login page of the identity server.
The documentation of Identity Server 4 state the use of
services.AddAuthentication()
.AddIdentityServerAuthentication("token", isAuth =>
{
isAuth.Authority = "base_address_of_identityserver";
isAuth.ApiName = "name_of_api";
});
as well as the use of
[Authorize(AuthenticationSchemes = "token")]
Unfortunatly the compiler state that .AddIdentityServerAuthentication can't be found. Do I miss a special nuget?
The nugets I use on the token server so far are:
IdentityServer4 (v2.2.0)
IdentityServer4.AspNetIdentity (v2.1.0)
IdentityServer4.EntityFramework (v2.1.1)
Figured out that part. The missing nuget for AddIdentityServerAuthentication is:
IdentityServer4.AccessTokenValidation
Struggling with the authorization based on the custom scope.
Does anyone know how the security has to be configured?
Configure a client with ClientGrantTypes = client_credentials and your api like this:
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5000";
options.ApiName = "api.auth";
});
Where ApiName is the name of the resource. Please note that resource != scope. In most samples the resource name is equal to the scope name. But not in your case, where resource name is api.auth and scope name is api.auth.endpoint1.
Configure the client to request the scope.
var tokenClient = new TokenClient(disco.TokenEndpoint, clientId, secret);
var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api.auth.endpoint1");
IdentityServer will lookup the Resource name and add that to the token as audience (aud) while the scope is added as claim with type scope.
This should be enough to make it work. Also check the sample project.
Custom authentication scheme and scope based policies for different access rights bundled together looks like that:
// Startup.ConfigureServices
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication("CustomAuthEndpointsAuthenticationScheme", options =>
{
options.Authority = "http://localhost:5000";
options.ApiName = "api.auth"; //IdentityServer4.Models.ApiResource.Name aka Audience
});
services.AddAuthorization(options =>
{
options.AddPolicy("Endpoint1Policy", policy => {
policy.AddAuthenticationSchemes(new string[] { "CustomAuthEndpointsAuthenticationScheme" });
policy.RequireScope("api.auth.endpoint1"); } ); //IdentityServer4.Models.Scope.Name
options.AddPolicy("Endpoint2Policy", policy => {
policy.AddAuthenticationSchemes(new string[] { "CustomAuthEndpointsAuthenticationScheme" });
policy.RequireScope("api.auth.endpoint2"); } ); //IdentityServer4.Models.Scope.Name
} );
// securing the custom endpoint controllers with different access rights
[Authorize(AuthenticationSchemes = "CustomAuthEndpointsAuthenticationScheme", Policy = "Endpoint1Policy")]
It seems not to interfere with the IdentityServer4 default endpoints nor with the ASP.NET Core Identity part.

Use ASP.Net Core 2 Identity with Azure Ad single tenant authentication

I would like to get help from community for one problem that I don't understand.
I create asp.net core 2 web application and I would like to configure the app to be able to login from the app via aspnetuser table or by using O365 Company account.
Then I followed multiple techniques described on the web included on MSDN website.
The app authentication works fine but Azure add returned : Error loading external login information.
I checked inside the code by generating identity views, the app failed on:
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
ErrorMessage = "Error loading external login information.";
return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
}
await _signInManager.GetExternalLoginInfoAsync(); return null and return the error message.
The application is correctly configured in azure AD and it work from my app if I remove the authentication from the app.
I configured my app middlewares as follow:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(AzureADDefaults.AuthenticationScheme).AddCookie()
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Authority = options.Authority + "/v2.0/";
options.TokenValidationParameters.ValidateIssuer = true;
});
And in configure method I added
app.UseAuthentication();
When I arrive on my login screen app (scaffolded by VS) all seems correct:
Login screen with two possibilities for authentication]:
Error message when i try Azure Active Directory method:
Can someone explain and help me to solve this problem?
Thanks in advance
The solution is to add cookieschemename as externalscheme. Below is sample code block in Startup.cs file.
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => { Configuration.Bind("AzureAd", options); options.CookieSchemeName = IdentityConstants.ExternalScheme; });
Unfortunately I had more or less the exact same problem. Although the Azure sample worked on its own, when I tried to integrate it to an existing application that uses Identity and other external authentication services, I could not get AzureAD to work. The interesting thing is that although in the output window I could see logging messages saying that the login was accomplished.
What I did (and this is more of a workaround rather than an exact solution to the problem) was to abandon using the Microsoft.AspNetCore.Authentication.AzureAD.UI package and I opted to go the longer way and configure OpenID manually for Azure. This article helped me immensely towards that end.
Having said that, I hope someone posts a more direct answer to your question.