.NET Core Model Binding issues - c#

I have an ASP.NET Core controller like this
[ApiController]
public class MADEController : Controller
{
private readonly IRepository _repository;
public MADEController( IRepository repository)
{
_repository = repository;
}
[HttpPost]
[Route("DKAPI/MADE/Update/{tblId}/{recId}")]
public HttpResponseMessage Update(string tblId, string recId, object formParams)
{
var temp = JsonConvert.SerializeObject(HttpContext.Request.Form.ToList());
}
}
If I try to call the Update action via Advanced Rest Client using as Request URL
http://localhost:10356/DKAPI/MADE/Update/32/5
and in the POST Body the following:
[{"Key":"formParams","Value":["[{\"key\":\"id\",\"value\":\"5\"},{\"key\":\"CarRegNo\",\"value\":\"HKK36512\"},{\"key\":\"CarEngSize\",\"value\":\"1234\"},{\"key\":\"DateCreated\",\"value\":\"19/09/2018 00:00\"},{\"key\":\"ExpenseStatus\",\"value\":\"U\"},{\"key\":\"DateCertified\",\"value\":\"\"},{\"key\":\"ClaimFrom\",\"value\":\"10/09/2018\"},{\"key\":\"ClaimTo\",\"value\":\"27/09/2018\"},{\"key\":\"TotalMilesuptothisclaim\",\"value\":\"\"},{\"key\":\"Staff\",\"value\":\"\"}]"]}]
tblId gets value 32, recId gets value 5 BUT the formParams object remains always empty. On the other hand the line
var temp = JsonConvert.SerializeObject(HttpContext.Request.Form.ToList());
prints the above mentioned object. Any ideas why that happens? Am I missing something on Model Binding in .NET Core? It used to work in .NET 4.6 1 but in .NET Core it fails
For convention I add here my startup.cs file.
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)
{
//Database connection
var connectionString = Configuration.GetConnectionString("DBConnectionStringName");
//SOS Add Cors before MVC
services.AddCors();
//Register Repositories
services.AddScoped<IRepository, Repository>();
//The following line is added in order to have access on the HttpContext in controllers
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services
.AddMvc()
.AddWebApiConventions()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddJsonOptions(options =>
{
options.SerializerSettings.ContractResolver = new DefaultContractResolver();
options.SerializerSettings.SerializationBinder = new DefaultSerializationBinder();
});
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
});
services.AddKendo();
services.AddTransient<IAuthorizationHandler, TokenHandler>();
services.AddAuthorization(options =>
{
options.AddPolicy("Token", policy =>
policy.Requirements.Add(new TokenRequirement()));
});
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = "DR";
options.AddScheme<DKSchemeHandler>("DR", "DataVision");
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseAuthentication();
app.UseOwin();
//SOS Add Cors before MVC
app.UseCors(x => x
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
app.UseMvc();
}
}
If in the Startup file I remove the lines
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressConsumesConstraintForFormFileParameters = true;
options.SuppressInferBindingSourcesForParameters = true;
options.SuppressModelStateInvalidFilter = true;
});
The action is never hit and I get a http 400 bad request error

Related

Intercept and read data before reaching the dotnet core ASP controllers

I have a standard asp dotnet core rest api application.
I want to read the data from the body before it arrives in the controllers to create a generic validation on that data.
I accept that I have to do this in Startup.cs, but I haven't found something similar for what I need.
STARTUP.CS
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.AddCors();
services.AddControllers();
}
// 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(errorApp =>
{
errorApp.Run(async context =>
{
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
context.Response.ContentType = "text/html";
var ex = context.Features.Get<IExceptionHandlerFeature>();
if (ex != null)
{
//var err = $"<h1>Erro: {ex.Error.Message + "<br><br>" + ex.Error.InnerException + "<br><br>" + ex.Error.StackTrace}</h1>";
var err = $"<p>Erro: {ex.Error.Message} </p>";
await context.Response.WriteAsync(err, System.Text.Encoding.GetEncoding("iso-8859-1")).ConfigureAwait(false);
//await context.Response.WriteAsync(err).ConfigureAwait(false);
}
});
});
app.UseHsts();
}
app.UseRouting();
app.UseCors(x => x
.AllowAnyMethod()
.AllowAnyHeader()
.SetIsOriginAllowed(origin => true)
.AllowCredentials());
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
I want to read the data from the body before it arrives in the controllers to create a generic validation on that data it sounds like you want to have an action filter. After creating the filter, pls inject the dependency into Startup.cs file.
services.AddControllers(config =>
{
config.Filters.Add<MySampleActionFilter>();
});
For example, I have an .net 6 api application, I created a filter like below:
using Microsoft.AspNetCore.Mvc.Filters;
namespace WebApiNet6
{
public class MyActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
// Do something before the action executes.
var req = context.HttpContext.Request;
}
public void OnActionExecuted(ActionExecutedContext context)
{
// Do something after the action executes.
}
}
}
Then I changed my Program.cs from builder.Services.AddControllers() to
builder.Services.AddControllers(options =>
{
options.Filters.Add<MyActionFilter>();
});
Then add a break point in the filter, when I call the api, it will go into the filter before go into the controller.

AuthorizeAttribute does nothing in ASP.NET Core 3 MVC

I have an ASP.NET Core 3.1 MVC project, with a simple authentication system based on cookies, and a few roles and policies. DI is handled with Autofac (not internal DI system of ASP.NET Core).
I know my user is correctly loaded with proper roles, and calls to internal methods like Context.User.IsInRole("Administrator")) (from views) are working as expected.
However, all my AuthorizeAttribute are not working, depending of the content of my Startup whether they seem to do nothing, or I am always redirected to login page (even if the right requirement is fulfilled).
An annotation like
[Authorize(Policy = "Administrator,Collaborator")]
or a simple
[Authorize(Roles = "Administrator")]
placed on an action method seems to do nothing.
I know that order of calls in startup Configure and ConfigureServices matter a lot, but despite many attempts and a lot of reading of similar questions on SO or somewhere else, I was not able to make it work as expected.
I share below my whole Startup file (sorry for that), in hope somebody will be able to point the correct order (or another kind of fix of course), to make it work for my specific case.
Thanks a lot.
public class Startup
{
public Startup(IConfiguration configuration, IWebHostEnvironment environment)
{
Configuration = configuration;
Environment = environment;
}
public IConfiguration Configuration { get; }
public IWebHostEnvironment Environment { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddRazorPages();
services.AddOptions();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Strict;
options.LoginPath = "/Account/Login";
options.SlidingExpiration = true;
options.ExpireTimeSpan = new TimeSpan(0, 24, 0, 0);
options.AccessDeniedPath = "/Error/RightError";
});
services.AddAuthorization(options =>
{
options.AddPolicy(SecurityPolicies.AdministratorOnly, policy =>
policy.RequireClaim(ClaimTypes.Role, UserRoles.Administrator));
options.AddPolicy(SecurityPolicies.AdministratorOrCollaborator, policy =>
policy.RequireClaim(ClaimTypes.Role, UserRoles.Administrator, UserRoles.Collaborator));
});
services.AddSession(options => options.IdleTimeout = TimeSpan.FromHours(4));
services.AddMvc(options => options.Filters.Add(new AuthorizeFilter()))
.AddNewtonsoftJson(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver());
services.AddKendo();
}
// ConfigureContainer is where you can register things directly
// with Autofac. This runs after ConfigureServices so the things
// here will override registrations made in ConfigureServices.
// Don't build the container; that gets done for you by the factory.
public void ConfigureContainer(ContainerBuilder builder)
{
builder.RegisterModule(new MyFrontModule());
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseExceptionHandler(ProcessError);
}
else
{
app.UseExceptionHandler("/Error/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSession();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseCookiePolicy();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllers();
endpoints.MapRazorPages();
});
var ci = new CultureInfo("fr-FR")
{
NumberFormat = { NumberDecimalSeparator = ".", CurrencyDecimalSeparator = "." }
};
app.UseRequestLocalization(new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture(ci),
SupportedCultures = new List<CultureInfo> { ci },
SupportedUICultures = new List<CultureInfo> { ci }
});
}
private void ProcessError(IApplicationBuilder appError)
{
appError.Run(async context =>
{
// Not relevant for my question
});
}
}
I also read that Json Serializer may change something, I am using Newtonsoft with DefaultContractResolver (as above), and I am using Telerik UI components.
Thanks a lot for any useful advice !
As explained in question, I have never been able to have Authorize attribute working as expected, so as my needs in right management were pretty simple, as a workaround I've just implemented a very straightforward FilterAttribute to verify rights based on owned roles.
public class RoleRequirementAttribute : TypeFilterAttribute
{
public RoleRequirementAttribute(params string[] claimValues)
: base(typeof(ClaimRequirementFilter))
{
Arguments = new []{ claimValues.Select(cv => new Claim(ClaimTypes.Role, cv)) };
}
}
public class ClaimRequirementFilter : IAuthorizationFilter
{
readonly IEnumerable<Claim> _claims;
public ClaimRequirementFilter(IEnumerable<Claim> claims)
{
_claims = claims;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
var hasClaim = context.HttpContext.User.Claims.Any(owned => _claims.Any(required => owned.Type == required.Type && owned.Value == required.Value));
if (!hasClaim)
{
context.Result = new ForbidResult();
}
}
}
I resolved this issue by adding .AddRoles<IdentityRole to the identity setting in the startup file.
services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();

How enable swagger for Asp.net core OData api

I have an asp.net core odata api. I want to enable swagger for this api. The version of asp.net core is 2.2 and the dependencies of this project are as below picture:
and the config of startup is as below:
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.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddDbContext<SeeMiddleContext>(options =>
{
options.UseSqlServer(
"Data Source = 192.168.1.1;Initial Catalog=Seem;persist security info=True;user id=Sas;password=Sas1");
});
services.AddOData();
services.AddSwaggerGen(options =>
{
options.DescribeAllEnumsAsStrings();
options.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info { Title = "eShopOnContainers - Catalog HTTP API", Version = "v1", Description = "The Catalog Microservice HTTP API. This is a DataDriven/CRUD microservice sample", TermsOfService = "Terms Of Service" });
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// 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.UseMvc(routeBuilder => {
routeBuilder.EnableDependencyInjection();
routeBuilder.Expand().Select().OrderBy().Filter().MaxTop(40);
});
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
}
}
When I run the api and when I type "/swagger" in the url, then I will encounter the error that is shown in below picture:
Try to add the following code in your ConfigureServices method
using Microsoft.AspNet.OData.Formatter;
using Microsoft.Net.Http.Headers;
public void ConfigureServices(IServiceCollection services)
{
// Workaround: https://github.com/OData/WebApi/issues/1177
services.AddMvcCore(options =>
{
foreach (var outputFormatter in options.OutputFormatters.OfType<ODataOutputFormatter>().Where(_ => _.SupportedMediaTypes.Count == 0))
{
outputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/prs.odatatestxx-odata"));
}
foreach (var inputFormatter in options.InputFormatters.OfType<ODataInputFormatter>().Where(_ => _.SupportedMediaTypes.Count == 0))
{
inputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/prs.odatatestxx-odata"));
}
});
}
For more details , you could refer to https://stackoverflow.com/a/51599466/10201850
For me, after all actions above, and described in the post, I still didn't have the odata-endpoint in swagger-ui page. The decorator
[ApiExplorerSettings(IgnoreApi = false)]
for my controller resolved the issue

Asp.net Core 2.2, custom middleware for authorizing files outside of wwwroot, but httpcontext.User is null

I need to allow access to a directory of static files only if the user is authenticated.
I have set up a middleware to check each request as it comes. I can get the request no problem, but the Httpcontext User is always null. I am using cookies authentication and not Identity. I have added the default authentication cookies scheme to the sign in as well as the services in startup.cs. I'm not sure why I can't get the user. This is based off of Scott Allen's tutorial https://odetocode.com/blogs/scott/archive/2015/10/06/authorization-policies-and-middleware-in-asp-net-5.aspx
Startup.cs
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.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddAuthentication( options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignOutScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.AccessDeniedPath = "/Home/Index";
options.LoginPath = "/Identity/Account/Login";
});
services.AddAuthorization(options =>
{
options.AddPolicy("Authenticated", policy => policy.RequireAuthenticatedUser());
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/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.UseProtectFolder(new ProtectFolderOptions
{
Path = "/StaticFiles",
PolicyName = "Authenticated",
});
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "Static_Files")),
RequestPath = "/StaticFiles"
});
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
private object RedirectResult()
{
throw new NotImplementedException();
}
}
MiddleWare
public class ProtectFolderOptions
{
public PathString Path { get; set; }
public string PolicyName { get; set; }
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class ProtectFolderExtensions
{
public static IApplicationBuilder UseProtectFolder(this IApplicationBuilder builder, ProtectFolderOptions options)
{
return builder.UseMiddleware<ProtectFolder>(options);
}
}
// You may need to install the Microsoft.AspNetCore.Http.Abstractions package into your project
public class ProtectFolder
{
private readonly RequestDelegate _next;
private readonly PathString _path;
private readonly string _policyName;
public ProtectFolder(RequestDelegate next,ProtectFolderOptions options)
{
_next = next;
_path = options.Path;
_policyName = options.PolicyName;
}
public async Task Invoke(HttpContext httpContext, IAuthorizationService authorizationService)
{
if (httpContext.Request.Path.StartsWithSegments(_path))
{
var authorized = await authorizationService.AuthorizeAsync(
httpContext.User, null, _policyName);
if (authorized.Succeeded == false)
{
await httpContext.ChallengeAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return;
}
}
await _next(httpContext);
}
}
The middleware is able to check the request. But httpcontext doesn't contain user even after I signed in, and I always get redirected to the login page.

ASP Net Core 2.1 Session

I am currently developing an API in .Net Core 2.1
with a client application in Vue 2 with Nuxt, and I have problems saving an object in session in ASP .Net.
I have reviewed this and other links before asking this question, but nothing has been able to help me.
It turns out that I've tried it with Postman and if it works, but I do not understand why it does not work with my application.
This is my Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => false;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
// Add Database
// End Add Database
services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigin", builder =>
builder.AllowAnyHeader()
.AllowAnyMethod()
.AllowAnyOrigin()
));
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(1440);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseCookiePolicy();
app.UseCors("AllowSpecificOrigin");
app.UseSession();
app.UseMvc();
}
}
In my controller:
[Route("api/customer/[controller]")]
[ApiController]
public class ClientController : ControllerBase { ...
... Get and set Session Var
var model = HttpContext.Session.GetString("User")
And other controller
HttpContext.Session.SetString("User", "Hello World")
HttpContext changes id every time I make a request for ajax, but postman does not change the Id and that's why I can recover the cookie.
You likely need to set the withCredentials flag when making your AJAX request. That shouldn't be required for same-site requests, but you mentioned CORS and didn't specify that it was same-site. With jQuery, that just means adding it to xhrFields in your your AJAX options object:
$.ajax({
...
xhrFields: {
withCredentials: true
}
});
Other libraries may have a different methodology, but all should have some way of setting this flag on the XMLHttpRequest object.

Categories