Asked by:
Kept running into 401 unauthorize response using .NET Core v3.1

Question
-
User1428199325 posted
I upgraded the source code to .NET Core v3.1 & I'm having trouble figuring how to debug the backend issue due to lot of dependency injections, abstractions & overriding classes/methods all over. The employee who wrote this have overcomplicate things & he had left the company so we got stuck with the confusing source code mess here that take a lot of our time & energy, to make sense of the it. :-/
The error I'm having is a 401 unauthorize response. I discovered the debugger doesnt respond in StartUp class when consuming the webservice, it only respond when you start up the Web App. So, it took us a while & finally found a hitting debugger breakpoint on a MVC controller page to point us in the right direction. There it is saying the Identity is not authenticated so that explain the unauthorize error.</div> <div> </div> <div>
We're not familiar with this one authentication technology, `Odachi`. We believe there are 2 seperate authentication architecture, which is ehe WebApp's webpages login authorization for the customer's web browser & Odachi deal with the WebApp's webservice login authorization for the 3rd party software making the webservice call.
Source code below is the webservice MVC controller w/ Authorization filter. Then further down will be the Startup w/ base Startup abstraction.
[ Webservice call ]
namespace ABC.Payments.AspNet.MVC
{
public class AuthorizeWithNoChallengeFilterAttribute : IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
if (context.HttpContext.User?.Identity.IsAuthenticated != true)
context.Result = new UnauthorizedResult();
}
}
}namespace ABC.Payments.MerchantWeb.Api
{
[TypeFilter(typeof(AuthorizeWithNoChallengeFilterAttribute))]
public class MerchantsV1Controller : Controller
{
[Route("api/v1/merchants/{merchantAccountId}/customers/payments/paymentmethods"), HttpPost]
public async Task<ObjectResult> Payment([FromBody] ItemInfoViewModel itemInfo, CancellationToken cancellationToken)
{
var payments = whatever();
return new HttpNotAcceptableObjectResult(payments);
}
}
}[ Startup & Base Startup ]
namespace ABC.Payments.MerchantWeb
{
// StackOverflow Post on how to find 401 Unathorize error (debug)
// --> https://stackoverflow.com/questions/43574552/authorization-in-asp-net-core-always-401-unauthorized-for-authorize-attribute
public class Startup : StartupBase<MerchantRequestContext, Merchant>
{
private const string _schemeCustomMerchantBasic = "CustomMerchantBasic";
public Startup(IWebHostEnvironment webHostEnvironment)
: base(webHostEnvironment, PortalRoleType.Merchant)
{
}
public void ConfigureServices(IServiceCollection services)
{
base._configureServices(true, services);
services.AddTransient(sp => sp.GetService<MerchantRequestContext>()?.Merchant);
services.AddTransient(sp => sp.GetService<MerchantRequestContext>()?.Customer);
services.AddTransient(sp => sp.GetService<MerchantRequestContext>()?.TenantSettings);
services.AddLocalization(options =>
{
options.ResourcesPath = "Resources";
});
services.AddMvcCore()
.AddViewLocalization(LanguageViewLocationExpanderFormat.SubFolder, setup =>
{
setup.ResourcesPath = "Resources";
})
.AddDataAnnotationsLocalization()
.AddApiExplorer();
services.AddCors(options =>
{
options.AddPolicy("Internal", p => p.WithOrigins(base._configuration["Cors:InternalSource"]).WithMethods("POST").WithHeaders("accept", "request", "authorization", "content-type", "internal"));
});
services.AddAuthentication()
// https://github.com/Kukkimonsuta/Odachi/blob/master/src/Odachi.AspNetCore.Authentication.Basic/Events/BasicSignInContext.cs (Basic Sign Context)
// https://github.com/Kukkimonsuta/Odachi/blob/master/samples/BasicAuthenticationSample/Startup.cs
.AddBasic(_schemeCustomMerchantBasic, options =>
{
// ////////Notice: AutomaticChallenge is depreciated, google search said to use DefaultChallengeScheme w/ given cookie-authentication-scheme but that still doesnt explain how to disable it
// //////// https://stackoverflow.com/questions/45878166/asp-net-core-2-0-disable-automatic-challenge
// //////// https://github.com/dotnet/aspnetcore/issues/2007
//## options.AutomaticChallenge = false;
options.Realm = "AutoPayment API v1";
options.Events = new BasicEvents()
{
OnSignIn = async context =>
{
var claims = new List<Claim>();
if (context.Username == "ndi3DanDba993nvbaqbn3d93" && context.Password == "aVd3Ed51dfDE5acCCni9l1IxPq9")
claims.Add(new Claim(ClaimTypes.Role, "InternalAPIUser"));
else
{
string merchantAccountId = context.Request.Path.Value.Split('/').Skip(4).FirstOrDefault();
var merchantRepository = context.HttpContext.RequestServices.GetRequiredService<IMerchantRepository>();
if (merchantAccountId == null || merchantAccountId.Length != 14 || merchantAccountId.Split('-').Length != 3)
throw new Exception($"Invalid merchant account Id ({merchantAccountId ?? string.Empty}).");
var merchant = await merchantRepository.GetMerchantAsync(merchantAccountId, context.HttpContext.RequestAborted);
if (merchant == null || !merchant.IsActive || (merchant.GatePayApiKey != context.Username || merchant.GatePayApiSecret != context.Password))
{
context.Fail("Invalid merchant"); //## context.HandleResponse();
return;
}
}
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name)); //## options.AuthenticationScheme));
context.Principal = principal;
//## context.Ticket = new AuthenticationTicket(principal, new AuthenticationProperties(), options.AuthenticationScheme);
context.Success(); //## context.HandleResponse();
//return Task.CompletedTask;
}
};
});
}
public void Configure(IApplicationBuilder applicationBuilder, ILoggerFactory loggerFactory, IServiceProvider serviceProvider)
{
base._configure(true, applicationBuilder, loggerFactory, serviceProvider);
applicationBuilder.UseCors("Internal");
applicationBuilder.UseWhen(context => !context.Request.Path.StartsWithSegments(new PathString("/api/v1")), b => b.UseAuthentication());
}
}
}
namespace ABC.Payments
{
public class StartupBase<TRequestContext, TUserContext>
where TRequestContext : RequestContext<TUserContext>
{
public StartupBase(IWebHostEnvironment webHostEnvironment, PortalRoleType portalRoleType)
{
_portalRoleType = portalRoleType;
_webHostEnvironment = webHostEnvironment;
var builder = new ConfigurationBuilder();
ConfigurationLoader.Load(builder, webHostEnvironment);
_configuration = builder.Build();
if (webHostEnvironment.EnvironmentName.Equals("Production", StringComparison.OrdinalIgnoreCase) == true && _configuration["ConfirmProduction"]?.Equals("Yes", StringComparison.OrdinalIgnoreCase) != true)
throw new Exception("Azure defaults to \"Production\" for the environment, so you need to create an AppSetting of \"ConfirmProduction\" to \"Yes\" to ensure that is the intent.");
}
private readonly IWebHostEnvironment _webHostEnvironment;
public readonly IConfiguration _configuration;
private readonly PortalRoleType _portalRoleType;
public void _configureServices(bool isWebBrowserFrontendGui, IServiceCollection services)
{
if (isWebBrowserFrontendGui)
{
services.AddDistributedRedisCache(options =>
{
options.Configuration = _configuration["Storage:Redis:Configuration"];
});
services.AddSingleton<RedisCache>();
services.AddSingleton<MemoryDistributedCache>();
services.AddSingleton<IDistributedCache>(
sp => new ResilientDistributedCache(sp.GetRequiredService<RedisCache>(), sp.GetRequiredService<MemoryDistributedCache>())
);
var azureBlobConnectionTring = _configuration["Storage:AzureBlob:ConnectionString"];
if (azureBlobConnectionTring != null)
{
var storageAccount = CloudStorageAccount.Parse(azureBlobConnectionTring);
var client = storageAccount.CreateCloudBlobClient();
var azureBlobContainer = client.GetContainerReference("dataprotection-key-container");
services.AddDataProtection().PersistKeysToAzureBlobStorage(azureBlobContainer, "keys.xml");
}
services.AddSession(options =>
{
//options.IdleTimeout = TimeSpan.FromMinutes(5);
});
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole<Guid>>()
.AddEntityFrameworkStores<ApplicationContext>() // FYI - AddEntityFrameworkStores() deal with role that derives from IdentityRole, as per documentation.
.AddDefaultTokenProviders();
services.ConfigureApplicationCookie(options => {
options.LoginPath = new PathString("/Home/Index");
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(_configuration.GetValue<int?>("Authentication:SlidingExpirationTime").Value);
options.AccessDeniedPath = new PathString("/Home/AccessDenied");
});
services.Configure<IdentityOptions>(options => {
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireDigit = false;
options.Password.RequiredLength = 7;
});
services.AddControllersWithViews();
services.AddRazorPages();
// AddMvc() vs AddMvcCore() explaination found at --> https://offering.solutions/blog/articles/2017/02/07/the-difference-between-addmvc-and-addmvccore/
// --> https://stackoverflow.com/questions/42365275/how-to-implement-a-pure-asp-net-core-web-api-by-using-addmvccore/42365276#42365276
services.AddMvc().AddRazorRuntimeCompilation();
services.Configure<MvcRazorRuntimeCompilationOptions>();
services.Configure<AuthorizationOptions>(options =>
{
options.DefaultPolicy = AuthorizationPolicy.Combine(options.DefaultPolicy,
new AuthorizationPolicy(new IAuthorizationRequirement[] {
new RolesAuthorizationRequirement(new string[] { _portalRoleType.ToString(), PortalRoleType.Internal.ToString() }),
new ImpersonationNotExpiredAuthorizationRequirement(_portalRoleType, _configuration.GetValue<TimeSpan?>("Authentication:ImpersonationTimeLimit").Value)
}, new string[0]));
});
services.AddMvcCore(options =>
{
var requestContextAttribute = new LoadRequestContextAttribute(typeof(TRequestContext));
options.Filters.Add(requestContextAttribute);
options.ModelBinderProviders[options.ModelBinderProviders.IndexOf(
options.ModelBinderProviders.OfType<ComplexTypeModelBinderProvider>().First()
)] = new TryServicesModelBinderProvider(services.BuildServiceProvider());
options.ModelBinderProviders.Insert(0, new EnumModelBinderProvider(services.BuildServiceProvider()));
})
.AddDataAnnotationsLocalization()
.AddNewtonsoftJson(settings =>
{
settings.SerializerSettings.ContractResolver = new DefaultContractResolver();
});
services.Configure<ForwardedHeadersOptions>(options => options.RequireHeaderSymmetry = false);
}
//services.AddPayments<TRequestContext, TUserContext>(_configuration, string.Empty);
}
public void _configure(bool isWebBrowserFrontendGui, IApplicationBuilder applicationBuilder, ILoggerFactory loggerFactory, IServiceProvider serviceProvider)
{
if (isWebBrowserFrontendGui)
{
serviceProvider.GetRequiredService<ITelemeter<StartupBase>>().TrackMetric("Startup Time", (DateTime.UtcNow - DateTime.UtcNow).TotalSeconds);
// Exception Page Handling.
if (!_webHostEnvironment.IsProduction())
{
applicationBuilder.UseDeveloperExceptionPage();
//applicationBuilder.UseDatabaseErrorPage();
}
else
applicationBuilder.UseExceptionHandler("/Home/ErrorPage.html");
applicationBuilder.UseStaticFiles(); // Note, we are not authenticating for static files if this is before them
//applicationBuilder.UseStatusCodePages();
// Session.
applicationBuilder.UseSession();
applicationBuilder.UseAuthentication();
// Routing.
applicationBuilder.UseRouting();
applicationBuilder.UseAuthorization(); // Exception error said to put this between UseRouting() & UseEnpoint().
applicationBuilder.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
// Config Localization.
var options = serviceProvider.GetService<IOptions<RequestLocalizationOptions>>();
if (options != null)
applicationBuilder.UseRequestLocalization(options.Value);
applicationBuilder.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All });
// Ensure Https.
var portals = applicationBuilder.ApplicationServices.GetRequiredService<Portals>();
applicationBuilder.Use(next => async httpContext =>
{
if (httpContext.Request.Host.Value.Contains("localhost"))
{
await next(httpContext);
}
else
{
string host = portals.GetHostForRedirect(httpContext.Request.Host.Value);
if (!host.Equals((httpContext.Request.IsHttps ? "https://" : "http://") + httpContext.Request.Host, StringComparison.OrdinalIgnoreCase))
httpContext.Response.Redirect($"{host}{httpContext.Request.Path}{httpContext.Request.QueryString}");
else
await next(httpContext);
}
});
}
//applicationBuilder.UsePayments<TRequestContext, TUserContext>();
}
}
}
Thursday, June 4, 2020 5:22 PM
All replies
-
User1428199325 posted
Can someone show me how do I fix the post formatting? This forum kept screwing up the formatter. Thanks.
Thursday, June 4, 2020 5:26 PM -
User2078676645 posted
Hi,
There is a code block button to the right of the top bar of the text box, you can paste the code into the code block and I will help you analyze the cause.
Regards,
Evern
Friday, June 5, 2020 9:39 AM -
User1428199325 posted
I saw that one & tried it. It made the muliple lines of code become 1 line of code & removed some spacing.
Also, every time I made a new post, it add wrapper to every line of text that mess it up too.So I think it is a forums.asp.net bug that is beyond my control.
On the bright side, I clean up the post (removed the <div> tags) to make it more readable for now.
Friday, June 5, 2020 12:42 PM -
User-474980206 posted
It’s a github open source repo.
https://github.com/Kukkimonsuta/Odachi
To debug, I’d fork it and add logging code for failure. Is the authentication missing, the user/password invalid or claims not loaded.Friday, June 5, 2020 2:46 PM