I am trying to request a bearer token from Azure to hit an endpoint on my ASP.NET core web application. I am using Microsoft.Identity platform to authenticate users to the web app which works just fine, but when I try and hit my web apps endpoints with something like PostMan I get the HTML of the Microsoft sign-in page returned to me...
I am able to get the expected response when I switch my authentication over to use Microsoft.AspNetCore.Authentication.JwtBearer (and configure AAD accordingly), but the reason I do not want to use JWT auth is that the web app needs to authenticate azure active directory users using role-based access. As far as I'm aware, that isn't an option when using JwtBearer auth?
I think my issue has to do with how I have configured AAD (Azure Active Directory), as it is not entirely clear how I need to configure my app registration I am using to authenticate within postman alongside the app registration I am using for defining my web apps authentication. To start off - I'll paste some code snippets below to demonstrate how my web app is configured for Microsoft.Identity auth.
Below is my Startup.cs file where I've configured Microsoft Identity:
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;
namespace Demo
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"));
services.AddAuthorization(options =>
{
// By default, all incoming requests will be authorized according to the default policy
options.FallbackPolicy = options.DefaultPolicy;
});
services.AddAuthorization(options =>
{
options.AddPolicy("AllRoles", policyBuilder => policyBuilder.RequireRole(new[] { "ADMIN", "USER" }));
options.AddPolicy("Admin", policyBuilder => policyBuilder.RequireRole(new[] { "ADMIN" }));
options.AddPolicy("User", policyBuilder => policyBuilder.RequireRole(new[] { "USER" }));
});
services.AddControllers();
services.AddControllersWithViews().AddRazorRuntimeCompilation();
services.AddRazorPages()
.AddMvcOptions(options => { })
.AddMicrosoftIdentityUI();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
});
}
}
}
Here is the code of the controller I am trying to hit:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Demo.Controllers
{
[Authorize("AllRoles")]
//[AllowAnonymous] // <-- This works fine
[Route("api/[controller]")]
[ApiController]
public class HealthCheckController : ControllerBase
{
[HttpGet]
public ActionResult<string> CheckConnectionToWebApp()
{
return "You've connected!";
}
}
}
Here is the relevant portion of my appsettings.json
...
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "qualified.domain.name", // Currently not set, unsure if this needs to be?
"TenantId": "00000000-0000-0000-0000-000000000000", // Set to a legitimate value in my code
"ClientId": "00000000-0000-0000-0000-000000000000", // Set to a legitimate value in my code
"CallbackPath": "/signin-oidc"
},
...
I have two app registrations set up... One that defines the authentication for my web app (its client ID sits in the appsettings.json above). When I say 'defines the authentication' I mean it literally defines the application roles I have listed in my startup.cs (ADMIN & USER) under the Manage->App Roles tab in AAD. User authentication works exactly as expected - users are denied access/allowed access to the UI pages based on the role they are assigned in AAD.
The second app registration I set up to use for authenticating postman to my web app. This app registration is not detailed anywhere in the C# webapp's code. It is configured to have the custom 'ADMIN' role assigned to it under the Manage->API Permissions tab in AAD. I assumed adding this API Permission would allow the app registration to gain access to the web app, but that does not seem to be the case... Below is the PostMan request I am trying to use to hit my Web Apps endpoint.
The PostMan request to retrieve a bearer token (this will return a token successfully):
POST https://login.microsoftonline.com/{TenantID}/oauth2/token
client_id:00000000-0000-0000-0000-000000000000 // Client ID of a secondary app registration I created & granted 'ADMIN' (where 'ADMIN' is my custom role defined in my Startup) API permissions too.
client_secret:xxx_xxxxxx-xxx_xxx__-xxx
grant_type:client_credentials
resource:api://00000000-0000-0000-0000-000000000000 // The application ID URI of the app registration defined on the app registration for my web app (the one used by the c# web app, which is defined in the appsettings.json).
The request I make to my web app using the token acquired above:
GET https://localhost:44304/api/healthcheck (with the bearer token in the header)
EXPECTED RESULT:
Status: 200 OK
Body: You've connected!
ACTUAL RESULT:
Status: 200 OK
Body:
<!-- Copyright (C) Microsoft Corporation. All rights reserved. -->
<!DOCTYPE html>
<html dir="ltr" class="" lang="en">
<head>
<title>Sign in to your account</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=2.0, user-scalable=yes">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="-1">
<link rel="preconnect" href="https://aadcdn.msauth.net" crossorigin>
<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="//aadcdn.msauth.net">
<link rel="dns-prefetch" href="//aadcdn.msftauth.net">
<meta name="PageID" content="ConvergedSignIn" />
<meta name="SiteID" content="" />
<meta name="ReqLC" content="1033" />
<meta name="LocLC" content="en-US" />
<meta name="referrer" content="origin" />
<noscript>
<meta http-equiv="Refresh" content="0; URL=https://login.microsoftonline.com/jsdisabled" />
</noscript>
<meta name="robots" content="none" />
<script type="text/javascript">
//<![CDATA[
$Config={ ...
No matter what I do, I cannot seem to get any response other than the above HTML sign-on page returned too me. Even to get a 401 response would be better than what I am currently receiving. Any ideas on what I may be overlooking?