i implemented custom media-type formatter with per request logic
public class JsonPermissionBasedFormatter : PartialJsonMediaTypeFormatter
{
public override MediaTypeFormatter GetPerRequestFormatterInstance(Type type, HttpRequestMessage request, MediaTypeHeaderValue mediaType)
{
User user = request.GetOwinContext()?.Request.Get<User>("AuthorizationFilter:CurrentUser");
var formatter = (PartialJsonMediaTypeFormatter)base.GetPerRequestFormatterInstance(type, request, mediaType);
formatter.SerializerSettings = SerializerSettings;
formatter.SerializerSettings.ContractResolver = new PermissionBasedContractResolver(user);
return formatter;
}
}
public class PermissionBasedContractResolver : DefaultContractResolver
{
private readonly User _user;
public PermissionBasedContractResolver(User user)
{
_user = user;
NamingStrategy = new CamelCaseNamingStrategy
{
ProcessDictionaryKeys = true,
OverrideSpecifiedNames = true
};
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
if (member == null)
{
throw new ArgumentNullException(nameof(member));
}
JsonProperty property = base.CreateProperty(member, memberSerialization);
var propertyInfo = member as PropertyInfo;
if (propertyInfo != null)
{
if (!PermissionsHelper.IsPropertyVisibleForUser(propertyInfo, _user))
{
property.ShouldSerialize = DoNotSerialize;
}
}
return property;
}
static bool DoNotSerialize(object o)
{
return false;
}
}
public static bool IsPropertyVisibleForUser(PropertyInfo info, User user)
{
if (info.GetCustomAttribute<IgnoreDataMemberAttribute>() != null)
return false;
if (user == null) return true;
var permissionForExportAttribute =
info.GetCustomAttribute<VisibleForAttribute>();
if (permissionForExportAttribute != null)
{
return
user.HasPermission(permissionForExportAttribute.Permission);
}
return true;
}
PermissionBasedContractResolver add custom logic for serialization which depend on user rights. For example user with admin rights receives additional properties in json, which ordinary user do not.
But if run requests in the following manner:
Parallel.For(1, 10000, _ =>
{
Get(ordinaryUser, isAdmin: false);
Get(adminUser, isAdmin: true);
});
occasionally got for ordinaryUser json properties availiable only for admins. i dont understand how it can happens.
Problem can be reproduce only under some load, if run requests manually via postman - all is ok. Can you tell what it can be or give advices how to investigate such problem.
I was wondering how I can achieve model validation with ASP.NET Web API. I have my model like so:
public class Enquiry
{
[Key]
public int EnquiryId { get; set; }
[Required]
public DateTime EnquiryDate { get; set; }
[Required]
public string CustomerAccountNumber { get; set; }
[Required]
public string ContactName { get; set; }
}
I then have a Post action in my API Controller:
public void Post(Enquiry enquiry)
{
enquiry.EnquiryDate = DateTime.Now;
context.DaybookEnquiries.Add(enquiry);
context.SaveChanges();
}
How do I add if(ModelState.IsValid) and then handle the error message to pass down to the user?
For separation of concern, I would suggest you use action filter for model validation, so you don't need to care much how to do validation in your api controller:
using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
namespace System.Web.Http.Filters
{
public class ValidationActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
var modelState = actionContext.ModelState;
if (!modelState.IsValid)
actionContext.Response = actionContext.Request
.CreateErrorResponse(HttpStatusCode.BadRequest, modelState);
}
}
}
Maybe not what you were looking for, but perhaps nice for someone to know:
If you are using .net Web Api 2 you could just do the following:
if (!ModelState.IsValid)
return BadRequest();
Depending on the model errors, you get this result:
{
Message: "The request is invalid."
ModelState: {
model.PropertyA: [
"The PropertyA field is required."
],
model.PropertyB: [
"The PropertyB field is required."
]
}
}
Like this, for example:
public HttpResponseMessage Post(Person person)
{
if (ModelState.IsValid)
{
PersonDB.Add(person);
return Request.CreateResponse(HttpStatusCode.Created, person);
}
else
{
// the code below should probably be refactored into a GetModelErrors
// method on your BaseApiController or something like that
var errors = new List<string>();
foreach (var state in ModelState)
{
foreach (var error in state.Value.Errors)
{
errors.Add(error.ErrorMessage);
}
}
return Request.CreateResponse(HttpStatusCode.Forbidden, errors);
}
}
This will return a response like this (assuming JSON, but same basic principle for XML):
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
(some headers removed here)
["A value is required.","The field First is required.","Some custom errorm essage."]
You can of course construct your error object/list any way you like, for example adding field names, field id's etc.
Even if it's a "one way" Ajax call like a POST of a new entity, you should still return something to the caller - something that indicates whether or not the request was successful. Imagine a site where your user will add some info about themselves via an AJAX POST request. What if the information they have tried to entered isn't valid - how will they know if their Save action was successful or not?
The best way to do this is using Good Old HTTP Status Codes like 200 OK and so on. That way your JavaScript can properly handle failures using the correct callbacks (error, success etc).
Here's a nice tutorial on a more advanced version of this method, using an ActionFilter and jQuery: http://asp.net/web-api/videos/getting-started/custom-validation
Or, if you are looking for simple collection of errors for your apps.. here is my implementation of this:
public override void OnActionExecuting(HttpActionContext actionContext)
{
var modelState = actionContext.ModelState;
if (!modelState.IsValid)
{
var errors = new List<string>();
foreach (var state in modelState)
{
foreach (var error in state.Value.Errors)
{
errors.Add(error.ErrorMessage);
}
}
var response = new { errors = errors };
actionContext.Response = actionContext.Request
.CreateResponse(HttpStatusCode.BadRequest, response, JsonMediaTypeFormatter.DefaultMediaType);
}
}
Error Message Response will look like:
{
"errors": [
"Please enter a valid phone number (7+ more digits)",
"Please enter a valid e-mail address"
]
}
You can use attributes from the System.ComponentModel.DataAnnotations namespace to set validation rules. Refer Model Validation - By Mike Wasson for details.
Also refer video ASP.NET Web API, Part 5: Custom Validation - Jon Galloway
Other References
Take a Walk on the Client Side with WebAPI and WebForms
How ASP.NET Web API binds HTTP messages to domain models, and how to work with media formats in Web API.
Dominick Baier - Securing ASP.NET Web APIs
Hooking AngularJS validation to ASP.NET Web API Validation
Displaying ModelState Errors with AngularJS in ASP.NET MVC
How to render errors to client? AngularJS/WebApi ModelState
Dependency-Injected Validation in Web API
Add below code in startup.cs file
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2).ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = (context) =>
{
var errors = context.ModelState.Values.SelectMany(x => x.Errors.Select(p => new ErrorModel()
{
ErrorCode = ((int)HttpStatusCode.BadRequest).ToString(CultureInfo.CurrentCulture),
ErrorMessage = p.ErrorMessage,
ServerErrorMessage = string.Empty
})).ToList();
var result = new BaseResponse
{
Error = errors,
ResponseCode = (int)HttpStatusCode.BadRequest,
ResponseMessage = ResponseMessageConstants.VALIDATIONFAIL,
};
return new BadRequestObjectResult(result);
};
});
C#
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.ModelState.IsValid == false)
{
actionContext.Response = actionContext.Request.CreateErrorResponse(
HttpStatusCode.BadRequest, actionContext.ModelState);
}
}
}
...
[ValidateModel]
public HttpResponseMessage Post([FromBody]AnyModel model)
{
Javascript
$.ajax({
type: "POST",
url: "/api/xxxxx",
async: 'false',
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
error: function (xhr, status, err) {
if (xhr.status == 400) {
DisplayModelStateErrors(xhr.responseJSON.ModelState);
}
},
....
function DisplayModelStateErrors(modelState) {
var message = "";
var propStrings = Object.keys(modelState);
$.each(propStrings, function (i, propString) {
var propErrors = modelState[propString];
$.each(propErrors, function (j, propError) {
message += propError;
});
message += "\n";
});
alert(message);
};
Here you can check to show the model state error one by one
public HttpResponseMessage CertificateUpload(employeeModel emp)
{
if (!ModelState.IsValid)
{
string errordetails = "";
var errors = new List<string>();
foreach (var state in ModelState)
{
foreach (var error in state.Value.Errors)
{
string p = error.ErrorMessage;
errordetails = errordetails + error.ErrorMessage;
}
}
Dictionary<string, object> dict = new Dictionary<string, object>();
dict.Add("error", errordetails);
return Request.CreateResponse(HttpStatusCode.BadRequest, dict);
}
else
{
//do something
}
}
}
I had an issue implementing the accepted solution pattern where my ModelStateFilter would always return false (and subsequently a 400) for actionContext.ModelState.IsValid for certain model objects:
public class ModelStateFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (!actionContext.ModelState.IsValid)
{
actionContext.Response = new HttpResponseMessage { StatusCode = HttpStatusCode.BadRequest};
}
}
}
I only accept JSON, so I implemented a custom model binder class:
public class AddressModelBinder : System.Web.Http.ModelBinding.IModelBinder
{
public bool BindModel(HttpActionContext actionContext, System.Web.Http.ModelBinding.ModelBindingContext bindingContext)
{
var posted = actionContext.Request.Content.ReadAsStringAsync().Result;
AddressDTO address = JsonConvert.DeserializeObject<AddressDTO>(posted);
if (address != null)
{
// moar val here
bindingContext.Model = address;
return true;
}
return false;
}
}
Which I register directly after my model via
config.BindParameter(typeof(AddressDTO), new AddressModelBinder());
You can also throw exceptions as documented here:
http://blogs.msdn.com/b/youssefm/archive/2012/06/28/error-handling-in-asp-net-webapi.aspx
Note, to do what that article suggests, remember to include System.Net.Http
Put this in the startup.cs file
services.AddMvc().ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = (context) =>
{
var errors = context.ModelState.Values.SelectMany(x => x.Errors.Select(p =>p.ErrorMessage)).ToList();
var result = new Response
{
Succeeded = false,
ResponseMessage = string.Join(", ",errors)
};
return new BadRequestObjectResult(result);
};
});
I have two controllers, AdminController and AccountController with the following code
AccountController:
[HttpPost]
public ActionResult LogOn(LogOnViewModel model)
{
if (ModelState.IsValid)
{
_authenticationService.SetPrincipal(model.UserName);
var exists = _authenticationService.ValidateCredentials(userName, password);
FormsAuthentication.SetAuthCookie(model.UserName, false);
if(exists){
return RedirectToAction("Index", "Admin");
}
}
return RedirectToAction("LogOn");
}
AdminController:
[Authenticate]
public class AdminController : Controller
{
[HttpGet]
public ActionResult Index()
{
return View();
}
}
AuthenticateAttribute is inherited from AuthorizeAttribute and has the following code:
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var authenticated = false;
if (HttpContext.Current.User != null && HttpContext.Current.User.Identity.IsAuthenticated)
{
//some actions
}
else
{
FormsAuthentication.SignOut();
FormsAuthentication.RedirectToLoginPage();
}
return authenticated;
}
_authenticationService is the instance of AuthenticationService class and SetPrincipal() method has the following code:
public void SetPrincipal(string userName)
{
var identity = new GenericIdentity(userName);
var principal = new GenericPrincipal(identity, null);
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null)
{
var ticket = new FormsAuthenticationTicket(1,
principal.Identity.Name,
DateTime.Now,
DateTime.Now.AddMinutes(30),
false,
String.Empty,
FormsAuthentication.FormsCookiePath);
string encryptedCookie = FormsAuthentication.Encrypt(ticket);
var authenticationCookie = HttpContext.Current.Response.Cookies[FormsAuthentication.FormsCookieName];
if (authenticationCookie != null)
{
authenticationCookie.Value = encryptedCookie;
authenticationCookie.Expires = DateTime.Now.AddMinutes(30);
}
HttpContext.Current.User = principal;
}
}
When I debug and watch AuthenticationService.SetPrincipal() HttpContext.Current.User.Identity.IsAuthenticated is true. But after redirect to Index action of AdminController in AuthenticateAttribute.AuthorizeAttribute() HttpContext.Current.User.Identity.IsAuthenticated is always false. As result I redirected to LogOn view again.
What am I doing wrong?
I don't see anywhere where you actually send the cookie back to the client. In order to be authenticated on each subsequent request, you have to send the encrypted cookie back to the client so that it can pass it back to your site.
HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedCookie);
Response.Cookies.Add(cookie);
I see where you try and get the current authentication cookie here:
var authenticationCookie = HttpContext.Current.Response.Cookies[FormsAuthentication.FormsCookieName];
But again, this is a GET, not a SET (or sending the cookie back) function line. At this point in your authentication, if you set a debugger, authenticationCookie is always going to be NULL.
Also, I don't see where you validate the password in any of your actions or functions. Ensure you are not overlooking that step.
One more thought/question/issue with your code. You are setting a variable called userExists in your controller action, but the function you call is a void type, so...you don't need to set that variable, just call the function.
_authenticationService.SetPrincipal(model.UserName);
return RedirectToAction("Index", "Admin");
This allows "frankl" to access but blocks the admins. What have I done wrong?
[Authorize(Order=1,Roles = "Admin",Users="frankl")]
public class AuthorizeBaseController_Admins_frank : Controller
{
}
It is probably simple but I don't see any examples that combine the two and the "Allowmultiple" property generates an error when I try to add it.
Thanks,
Chris
Roles and Users should be used exclusively. If you want to combine them you could write a custom authorize attribute:
public class MyAuthoirizeAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException("httpContext");
}
var user = httpContext.User;
if (!user.Identity.IsAuthenticated)
{
return false;
}
var usersSplit = SplitString(Users);
var rolesSplit = SplitString(Roles);
return
(usersSplit.Length > 0 && usersSplit.Contains(user.Identity.Name, StringComparer.OrdinalIgnoreCase)) ||
(rolesSplit.Length > 0 && rolesSplit.Any(user.IsInRole));
}
private string[] SplitString(string original)
{
if (string.IsNullOrEmpty(original))
{
return new string[0];
}
return (from piece in original.Split(',')
let trimmed = piece.Trim()
where !string.IsNullOrEmpty(trimmed)
select trimmed).ToArray();
}
}
and then:
[MyAuthorize(Order = 1, Roles = "Admin", Users="frankl")]
public class AuthorizeBaseController_Admins_frank : Controller
{
...
}
Unfortunately the AuthorizeAttribrute will let you either specify valid users, or valid roles - not both. Here is the relevant bit of code from the MVC 3 source.
protected virtual bool AuthorizeCore(HttpContextBase httpContext) {
if (httpContext == null) {
throw new ArgumentNullException("httpContext");
}
IPrincipal user = httpContext.User;
if (!user.Identity.IsAuthenticated) {
return false;
}
if (_usersSplit.Length > 0 && !_usersSplit.Contains(user.Identity.Name, StringComparer.OrdinalIgnoreCase)) {
return false;
}
if (_rolesSplit.Length > 0 && !_rolesSplit.Any(user.IsInRole)) {
return false;
}
return true;
}
You will either need to make 'frankl' an Admin, or create a custom authorization attribrute
I am creating my own custom authorize attribute, overriding the AuthorizeCore method and wanted to know if it is possible to access the Roles which have been passed into the authorize attribute tag.
So for instance if I have this:
[CustomAuthorize(Roles = "Administrator, Sales, Entry")]
Is it possible to access these from inside here:
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
}
I could then split the string and create an array.
You can this this.Roles which is a string that you need to split.
The source code is freely available.
The default AuthorizeCore implementation:
protected virtual bool AuthorizeCore(HttpContextBase httpContext) {
if (httpContext == null) {
throw new ArgumentNullException("httpContext");
}
IPrincipal user = httpContext.User;
if (!user.Identity.IsAuthenticated) {
return false;
}
if (_usersSplit.Length > 0 && !_usersSplit.Contains(user.Identity.Name, StringComparer.OrdinalIgnoreCase)) {
return false;
}
if (_rolesSplit.Length > 0 && !_rolesSplit.Any(user.IsInRole)) {
return false;
}
return true;
}
And they have an internal split function which looks like this:
internal static string[] SplitString(string original) {
if (String.IsNullOrEmpty(original)) {
return new string[0];
}
var split = from piece in original.Split(',')
let trimmed = piece.Trim()
where !String.IsNullOrEmpty(trimmed)
select trimmed;
return split.ToArray();
}