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.
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 & Startup Base ]
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>();
}
}
}