<div>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. :-/</div> <div> </div> <div>
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.</div> <div> </div> <div>
Source code below is the webservice MVC controller w/ Authorization filter. Then further down will be the Startup w/ base Startup abstraction.</div> <div></div> <div>
[ Webservice call ]</div> <div></div> <div>
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();
}
}
}
</div> <div></div> <div>
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);
}
}
}
</div> <div></div> <div>
[ Startup & Base Startup ]
</div> <div></div> <div> <div>
namespace ABC.Payments.MerchantWeb</div> <div>
{</div> <div>
// StackOverflow Post on how to find 401 Unathorize error (debug)</div> <div>
// --> https://stackoverflow.com/questions/43574552/authorization-in-asp-net-core-always-401-unauthorized-for-authorize-attribute</div> <div> </div> <div> public class Startup : StartupBase<MerchantRequestContext,
Merchant></div> <div>
{</div> <div>
private const string _schemeCustomMerchantBasic = "CustomMerchantBasic";</div> <div> </div> <div>
public Startup(IWebHostEnvironment webHostEnvironment)</div> <div> : base(webHostEnvironment, PortalRoleType.Merchant)</div> <div>
{</div> <div>
}</div> <div> </div> <div>
public void ConfigureServices(IServiceCollection services)</div> <div>
{</div> <div>
base._configureServices(true, services);</div> <div> </div> <div>
services.AddTransient(sp => sp.GetService<MerchantRequestContext>()?.Merchant);</div> <div>
services.AddTransient(sp => sp.GetService<MerchantRequestContext>()?.Customer);</div> <div>
services.AddTransient(sp => sp.GetService<MerchantRequestContext>()?.TenantSettings);</div> <div> </div> <div>
services.AddLocalization(options =></div> <div>
{</div> <div>
options.ResourcesPath = "Resources";</div> <div>
});</div> <div>
services.AddMvcCore()</div> <div>
.AddViewLocalization(LanguageViewLocationExpanderFormat.SubFolder, setup =></div> <div>
{</div> <div>
setup.ResourcesPath = "Resources";</div> <div>
})</div> <div>
.AddDataAnnotationsLocalization()</div> <div>
.AddApiExplorer();</div> <div> </div> <div>
services.AddCors(options =></div> <div>
{</div> <div>
options.AddPolicy("Internal", p => p.WithOrigins(base._configuration["Cors:InternalSource"]).WithMethods("POST").WithHeaders("accept", "request", "authorization", "content-type", "internal"));</div> <div>
});</div> <div> </div> <div>
services.AddAuthentication()</div> <div>
// https://github.com/Kukkimonsuta/Odachi/blob/master/src/Odachi.AspNetCore.Authentication.Basic/Events/BasicSignInContext.cs (Basic Sign Context)</div> <div>
// https://github.com/Kukkimonsuta/Odachi/blob/master/samples/BasicAuthenticationSample/Startup.cs</div> <div>
.AddBasic(_schemeCustomMerchantBasic, options =></div> <div>
{</div> <div>
// ////////Notice: AutomaticChallenge is depreciated, google search said to use DefaultChallengeScheme w/ given cookie-authentication-scheme but that still doesnt explain how to disable it</div> <div>
// //////// https://stackoverflow.com/questions/45878166/asp-net-core-2-0-disable-automatic-challenge</div> <div>
// //////// https://github.com/dotnet/aspnetcore/issues/2007</div> <div>
//## options.AutomaticChallenge = false;</div> <div>
options.Realm = "AutoPayment API v1";</div> <div>
options.Events = new BasicEvents()</div> <div>
{</div> <div>
OnSignIn = async context =></div> <div>
{</div> <div>
var claims = new List<Claim>();</div> <div> </div> <div>
if (context.Username == "ndi3DanDba993nvbaqbn3d93" && context.Password == "aVd3Ed51dfDE5acCCni9l1IxPq9")</div> <div>
claims.Add(new Claim(ClaimTypes.Role, "InternalAPIUser"));</div> <div>
else</div> <div>
{</div> <div>
string merchantAccountId = context.Request.Path.Value.Split('/').Skip(4).FirstOrDefault();</div> <div>
var merchantRepository = context.HttpContext.RequestServices.GetRequiredService<IMerchantRepository>();</div> <div> </div> <div>
if (merchantAccountId == null || merchantAccountId.Length != 14 || merchantAccountId.Split('-').Length != 3)</div> <div>
throw new Exception($"Invalid merchant account Id ({merchantAccountId ?? string.Empty}).");</div> <div> </div> <div>
var merchant = await merchantRepository.GetMerchantAsync(merchantAccountId, context.HttpContext.RequestAborted);</div> <div>
if (merchant == null || !merchant.IsActive || (merchant.GatePayApiKey != context.Username || merchant.GatePayApiSecret != context.Password))</div> <div>
{</div> <div>
context.Fail("Invalid merchant"); //## context.HandleResponse();</div> <div>
return;</div> <div>
}</div> <div>
}</div> <div> </div> <div>
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name)); //## options.AuthenticationScheme));</div> <div>
context.Principal = principal;</div> <div> </div> <div>
//## context.Ticket = new AuthenticationTicket(principal, new AuthenticationProperties(), options.AuthenticationScheme);</div> <div>
context.Success(); //## context.HandleResponse();</div> <div>
//return Task.CompletedTask;</div> <div>
}</div> <div>
};</div> <div>
});</div> <div>
}</div> <div> </div> <div>
public void Configure(IApplicationBuilder applicationBuilder, ILoggerFactory loggerFactory, IServiceProvider serviceProvider)</div> <div>
{</div> <div>
base._configure(true, applicationBuilder, loggerFactory, serviceProvider);</div> <div> </div> <div>
applicationBuilder.UseCors("Internal");</div> <div> </div> <div>
applicationBuilder.UseWhen(context => !context.Request.Path.StartsWithSegments(new PathString("/api/v1")), b => b.UseAuthentication());</div> <div>
}</div> <div>
}</div> <div>
}</div>
<div> namespace ABC.Payments</div> <div>
{</div> <div>
public class StartupBase<TRequestContext, TUserContext> </div> <div>
where TRequestContext : RequestContext<TUserContext></div> <div>
{</div> <div>
public StartupBase(IWebHostEnvironment webHostEnvironment, PortalRoleType portalRoleType)</div> <div>
{</div> <div>
_portalRoleType = portalRoleType;</div> <div>
_webHostEnvironment = webHostEnvironment;</div> <div> </div> <div>
var builder = new ConfigurationBuilder();</div> <div>
ConfigurationLoader.Load(builder, webHostEnvironment);</div> <div>
_configuration = builder.Build();</div> <div> </div> <div>
if (webHostEnvironment.EnvironmentName.Equals("Production", StringComparison.OrdinalIgnoreCase) == true && _configuration["ConfirmProduction"]?.Equals("Yes", StringComparison.OrdinalIgnoreCase) != true)</div> <div>
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.");</div> <div>
}</div> <div> </div> <div>
private readonly IWebHostEnvironment _webHostEnvironment;</div> <div>
public readonly IConfiguration _configuration;</div> <div>
private readonly PortalRoleType _portalRoleType;</div> <div> </div> <div>
public void _configureServices(bool isWebBrowserFrontendGui, IServiceCollection services)</div> <div>
{</div> <div>
if (isWebBrowserFrontendGui)</div> <div>
{</div> <div>
services.AddDistributedRedisCache(options =></div> <div>
{</div> <div>
options.Configuration = _configuration["Storage:Redis:Configuration"];</div> <div>
});</div> <div>
services.AddSingleton<RedisCache>();</div> <div>
services.AddSingleton<MemoryDistributedCache>();</div> <div>
services.AddSingleton<IDistributedCache>(</div> <div>
sp => new ResilientDistributedCache(sp.GetRequiredService<RedisCache>(), sp.GetRequiredService<MemoryDistributedCache>())</div> <div>
);</div> <div> </div> <div>
var azureBlobConnectionTring = _configuration["Storage:AzureBlob:ConnectionString"];</div> <div>
if (azureBlobConnectionTring != null)</div> <div>
{</div> <div>
var storageAccount = CloudStorageAccount.Parse(azureBlobConnectionTring);</div> <div>
var client = storageAccount.CreateCloudBlobClient();</div> <div>
var azureBlobContainer = client.GetContainerReference("dataprotection-key-container");</div> <div>
services.AddDataProtection().PersistKeysToAzureBlobStorage(azureBlobContainer, "keys.xml");</div> <div>
}</div> <div> </div> <div>
services.AddSession(options =></div> <div>
{</div> <div>
//options.IdleTimeout = TimeSpan.FromMinutes(5);</div> <div>
});</div> <div> </div> <div>
services.AddDefaultIdentity<ApplicationUser>()</div> <div>
.AddRoles<IdentityRole<Guid>>()</div> <div>
.AddEntityFrameworkStores<ApplicationContext>() // FYI - AddEntityFrameworkStores() deal with role that derives from IdentityRole, as per documentation.</div> <div>
.AddDefaultTokenProviders();</div> <div>
services.ConfigureApplicationCookie(options => {</div> <div>
options.LoginPath = new PathString("/Home/Index");</div> <div>
options.SlidingExpiration = true;</div> <div>
options.ExpireTimeSpan = TimeSpan.FromMinutes(_configuration.GetValue<int?>("Authentication:SlidingExpirationTime").Value);</div> <div>
options.AccessDeniedPath = new PathString("/Home/AccessDenied");</div> <div>
});</div> <div>
services.Configure<IdentityOptions>(options => {</div> <div>
options.Password.RequireUppercase = false;</div> <div>
options.Password.RequireLowercase = false;</div> <div>
options.Password.RequireNonAlphanumeric = false;</div> <div>
options.Password.RequireDigit = false;</div> <div>
options.Password.RequiredLength = 7;</div> <div>
});</div> <div> </div> <div>
services.AddControllersWithViews();</div> <div>
services.AddRazorPages();</div> <div> </div> <div>
// AddMvc() vs AddMvcCore() explaination found at --> https://offering.solutions/blog/articles/2017/02/07/the-difference-between-addmvc-and-addmvccore/</div> <div>
// --> https://stackoverflow.com/questions/42365275/how-to-implement-a-pure-asp-net-core-web-api-by-using-addmvccore/42365276#42365276</div> <div>
services.AddMvc().AddRazorRuntimeCompilation();</div> <div> </div> <div>
services.Configure<MvcRazorRuntimeCompilationOptions>();</div> <div>
services.Configure<AuthorizationOptions>(options =></div> <div>
{</div> <div>
options.DefaultPolicy = AuthorizationPolicy.Combine(options.DefaultPolicy,</div> <div>
new AuthorizationPolicy(new IAuthorizationRequirement[] {</div> <div>
new RolesAuthorizationRequirement(new string[] { _portalRoleType.ToString(), PortalRoleType.Internal.ToString() }),</div> <div>
new ImpersonationNotExpiredAuthorizationRequirement(_portalRoleType, _configuration.GetValue<TimeSpan?>("Authentication:ImpersonationTimeLimit").Value)</div> <div>
}, new string[0]));</div> <div>
});</div> <div> </div> <div>
services.AddMvcCore(options =></div> <div>
{</div> <div>
var requestContextAttribute = new LoadRequestContextAttribute(typeof(TRequestContext));</div> <div>
options.Filters.Add(requestContextAttribute); </div> <div> </div> <div> </div> <div>
options.ModelBinderProviders[options.ModelBinderProviders.IndexOf(</div> <div> </div> <div>
options.ModelBinderProviders.OfType<ComplexTypeModelBinderProvider>().First()</div> <div> </div> <div>
)] = new TryServicesModelBinderProvider(services.BuildServiceProvider());</div> <div>
options.ModelBinderProviders.Insert(0, new EnumModelBinderProvider(services.BuildServiceProvider()));</div> <div>
})</div> <div> </div> <div>
.AddDataAnnotationsLocalization()</div> <div>
.AddNewtonsoftJson(settings =></div> <div>
{</div> <div>
settings.SerializerSettings.ContractResolver = new DefaultContractResolver();</div> <div>
});</div> <div> </div> <div>
services.Configure<ForwardedHeadersOptions>(options => options.RequireHeaderSymmetry = false);</div> <div>
}</div> <div> </div> <div>
//services.AddPayments<TRequestContext, TUserContext>(_configuration, string.Empty);</div> <div>
}</div> <div> </div> <div>
public void _configure(bool isWebBrowserFrontendGui, IApplicationBuilder applicationBuilder, ILoggerFactory loggerFactory, IServiceProvider serviceProvider)</div> <div>
{</div> <div>
if (isWebBrowserFrontendGui)</div> <div>
{</div> <div>
serviceProvider.GetRequiredService<ITelemeter<StartupBase>>().TrackMetric("Startup Time", (DateTime.UtcNow - DateTime.UtcNow).TotalSeconds);</div> <div> </div> <div>
// Exception Page Handling.</div> <div>
if (!_webHostEnvironment.IsProduction())</div> <div>
{</div> <div>
applicationBuilder.UseDeveloperExceptionPage();</div> <div>
//applicationBuilder.UseDatabaseErrorPage();</div> <div>
}</div> <div>
else</div> <div>
applicationBuilder.UseExceptionHandler("/Home/ErrorPage.html");</div> <div> </div> <div>
applicationBuilder.UseStaticFiles(); // Note, we are not authenticating for static files if this is before them</div> <div>
//applicationBuilder.UseStatusCodePages();</div> <div> </div> <div>
// Session.</div> <div>
applicationBuilder.UseSession();</div> <div> </div> <div>
applicationBuilder.UseAuthentication();</div> <div> </div> <div>
// Routing.</div> <div>
applicationBuilder.UseRouting();</div> <div>
applicationBuilder.UseAuthorization(); // Exception error said to put this between UseRouting() & UseEnpoint().</div> <div>
applicationBuilder.UseEndpoints(endpoints =></div> <div>
{</div> <div>
endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");</div> <div>
endpoints.MapRazorPages();</div> <div>
});</div> <div> </div> <div>
// Config Localization.</div> <div>
var options = serviceProvider.GetService<IOptions<RequestLocalizationOptions>>();</div> <div>
if (options != null)</div> <div>
applicationBuilder.UseRequestLocalization(options.Value);</div> <div> </div> <div>
applicationBuilder.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All });</div> <div> </div> <div>
// Ensure Https.</div> <div>
var portals = applicationBuilder.ApplicationServices.GetRequiredService<Portals>();</div> <div>
applicationBuilder.Use(next => async httpContext =></div> <div>
{</div> <div>
if (httpContext.Request.Host.Value.Contains("localhost"))</div> <div>
{</div> <div>
await next(httpContext);</div> <div>
}</div> <div>
else</div> <div>
{</div> <div>
string host = portals.GetHostForRedirect(httpContext.Request.Host.Value);</div> <div>
if (!host.Equals((httpContext.Request.IsHttps ? "https://" : "http://") + httpContext.Request.Host, StringComparison.OrdinalIgnoreCase))</div> <div>
httpContext.Response.Redirect($"{host}{httpContext.Request.Path}{httpContext.Request.QueryString}");</div> <div>
else</div> <div>
await next(httpContext);</div> <div>
}</div> <div>
});</div> <div>
}</div> <div> </div> <div>
//applicationBuilder.UsePayments<TRequestContext, TUserContext>();</div> <div>
}</div> <div>
}</div> <div>
}</div> <div></div> </div>