I have a Blazor server application that uses Azure AD login. This works fine, but when setting [Authorize] on my API controller, the controller returns 401 unauthorized event though the Bearer token is passed.
I can see the token being passed in the API, so I am kind of stuck. Problem with config or some code error? Any help would be appreciated.
Azure config for Blazor project:
Azure setup for Blazor (server side project):
Azure setup for Blazor (server side project)
Auth setup for Blazor: Auth setup for Blazor
Secret added for Blazor: Secret added for Blazor
Api permissions for Blazor: Api permissions for Blazor
Azure config for API project:
Azure setup for API: Azure setup for API
Auth setup for API: Auth setup for API
API permissions for API (probably too many, found a guide that suggested adding more): API permissions for API (probably too many, found a guide that suggested adding more)
Exposed an API for API (type-o in admin.accsess): Exposed an API for API (type-o in admin.accsess)
Code for blazor project:
appsettings.json (new secret will be made and moved to keyvault as soon as this works): `
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"ClientId": "3c 8c",
"TenantId": "eb a0",
"Scopes": "api://3c 8c/admin.accsess"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
`
Startup.cs `
public void ConfigureServices(IServiceCollection services)
{
services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi(new string[] { Configuration["AzureAd.Scopes"] })
.AddInMemoryTokenCaches();
services.AddHttpContextAccessor();
services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
services.AddAuthorization(options =>
{
// By default, all incoming requests will be authorized according to the default policy
options.FallbackPolicy = options.DefaultPolicy;
});
services.AddRazorPages();
services.AddServerSideBlazor()
.AddMicrosoftIdentityConsentHandler();
[...]
}
`
`
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.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
`
Test controller for attempting the api call (always get 401 from api): `
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Diagnostics;
using Microsoft.Identity.Web;
using Newtonsoft.Json.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
namespace S********.Controllers
{
[Authorize]
public class TenantController : Controller
{
private readonly IHttpClientFactory _clientFactory;
private readonly ITokenAcquisition _tokenAcquisitionService;
private HttpClient _client;
public TenantController(IHttpClientFactory clientFactory, ITokenAcquisition tokenAcquisitionService)
{
_clientFactory = clientFactory;
_tokenAcquisitionService = tokenAcquisitionService;
}
public IActionResult Index()
{
return View();
}
public async Task<IActionResult> GetStuff()
{
string[] scopes = { "api://3c*******************c8c/admin.accsess" };
string accessToken = await _tokenAcquisitionService.GetAccessTokenForUserAsync(scopes);
_client = _clientFactory.CreateClient();
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = await _client.GetAsync("https://localhost:44394/api/SystemConfig/GetByName/System.Tenants");
return Ok(response);
}
}
}
`
Code for API project:
appsettings.json:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "custodisas.onmicrosoft.com",
"TenantId": "eb a0",
"ClientId": "7d de",
"ClientSecret": "G 5",
"CallbackPath": "/auth-response"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
Startup.cs:
`
public void ConfigureServices(IServiceCollection services)
{
var NlogSetup = new SetupLogger();
NLogConfig = NlogSetup.SetUpLogger("C://Logtest//Skynet//SiloApi.txt").Result;
LogManager.Configuration = NLogConfig;
Logger = LogManager.GetLogger("SkynetAPI");
Logger.Info("Logger up and running");
services.AddAuthentication(AzureADDefaults.JwtBearerAuthenticationScheme)
.AddAzureADBearer(options => Configuration.Bind("AzureAd", options));
// Have tried both with and without scope as required claim
var scopePolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
//.RequireClaim("scope", "api://3c7*****************c8c/admin.accsess")
.Build();
// Have tried both with and without adding scopepolicy as filter
services.AddMvc(options =>
{
//options.Filters.Add(new AuthorizeFilter(scopePolicy));
options.EnableEndpointRouting = false;
});
[...other services...]
services.AddControllers();
[...other services...]
}
`
`
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseMvc(routes =>
{
routes.MapRoute(name: "default", template: "api/{controller}/{action}/{id?}");
});
}
`
Controller beind called (returns correct info when removing [Authorize]): `
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web.Resource;
using Newtonsoft.Json;
using NLog;
using Orleans;
using *****.Grains.Interfaces;
using *****.Grains.Interfaces.Core.ApplicationScheduler;
using *****.Shared.Core;
using *****.Shared.Core.ApplicationScheduler;
using *****.Shared.Core.GrainStates;
using *****.Shared.Core.RunApplication;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Identity.Web;
using Microsoft.AspNetCore.Cors;
using System.IdentityModel.Tokens.Jwt;
namespace *****.API.Controllers
{
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class SystemConfigController : MasterController
{
public SystemConfigController(Logger logger, IClusterClient clusterClient) : base(logger, clusterClient)
{
}
[HttpGet]
[Route("GetByName/{configGrainName}")]
public async Task<ActionResult<string>> GetSystemConfigByName(string configGrainName)
{
// Tried adding this code to get the token, it is passed and I can see it when removing [Authorize]
var re = Request;
var headers = re.Headers;
var tokenString = headers["Authorization"];
Logger.Info("Token: \n\n" + tokenString + "\n\n");
var tmpGrain = clusterClient.GetGrain<I*****>(0);
return JsonConvert.SerializeObject(await tmpGrain.GetSystemGrainByName(configGrainName));
}
}
}
`
MasterController: `
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NLog;
using Orleans;
namespace *****.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class MasterController : ControllerBase
{
// TODO: Get from config.
public Guid SystemID { get; set; } = Guid.Parse("a9****************71c");
public Logger Logger;
public IClusterClient clusterClient;
public MasterController(Logger logger, IClusterClient client)
{
Logger = logger;
clusterClient = client;
}
}
}
`
Hope this is enough info. I am completely stuck, so any help would be appreciated. Thank you :)