Secure REST services in ASP .NET: input validation and exception handling.
One of the basic principles of a good REST API is input validation and clear communication of validation errors to the service consuming the API. In ASP, we have dedicated attributes and the ProblemDetails class compliant with the RFC 7807 standard.
RFC 7807 standard
In the API, we almost always return errors that we can signal via HTTP codes, but often such information is only a basic indicator of the error, not allowing us to determine the exact cause. Therefore, in March 2016, a standard was defined to unify the return of errors by API interfaces.
The definition in accordance with RFC 7807, for example, should look like this:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-52f8eaa452b5b1ab506c5abb29e7e6e7-5aa2a02d9a445576-00",
"errors": {
"Title": [
"The Title field is required.",
"The field Title must be a string with a minimum length of 5 and a maximum length of 15."
]
}
}
Where the most interesting section for us is the Errors board. Property Title according to the specification, it should be a short message that is readable by humans. The standard also provides for the possibility of locating the error title.
Of course, simply returning the appropriate JSON does not solve the problem, we still have to remember to return the appropriate HTTP response code, in the above example 400 - BadRequest, we also represent it through the property Status
Validation with Attributes in ASP
Redmond developers starting with .NET Core 2.1 default for an API project to return a class that implements the above standard when using Validation Attributes. Therefore, for most cases, it is enough to use dedicated validation attributes in the model class that we accept in the Body of the API method.
For example
Model class
public class TodoItem
{
[Required]
[StringLength(15, MinimumLength = 5)]
public string Title { get; set; }
[Required]
public DateTime ValidTo { get; set; }
[MinLength(5)]
public string SubTitle { get; set; }
[EmailAddress]
public string NotifyEmail { get; set; }
}
Controller
//...
public class TodoController : ControllerBase
{
//...
[HttpPost]
public void Post([FromBody] TodoItem value)
{ }
//...
}
After calling the above method with invalid input, we will get an error:
Request
{
"title": "Test",
"validTo": "2022-06-07T18:45:49.742Z",
"subTitle": "string",
"notifyEmail": "userexample"
}
Response
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-a70409b3919da21ece51e7c524db7684-80db0ffd6d864f8e-00",
"errors": {
"NotifyEmail": [
"The NotifyEmail field is not a valid e-mail address."
]
}
}
Built-in attributes
Sample attributes available in the namespace System.ComponentModel.DataAnnotations:
[Required]– Required field[EmailAddress]– Email type field[StringLength]– Character length limitation[RegularExpression]– Regular expression
Own validation attribute
The built-in attributes do not always cover our cases, then the ability to implement your own attributes comes in handy. For validation attributes we need to implement an abstract class ValidationAttribute
Example
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public class ContainsA: ValidationAttribute
{
internal static string ErrorMessageMockup => "String don't contain A letter";
public ContainsA() : base(() => ErrorMessageMockup) { }
public override bool IsValid(object? value)
{
if (!(value is string) || value == null)
return false;
return ((string)value).Contains("A");
}
}
The above attribute will, of course, check for strings whether they contain at least one "A" character.
Validation in the controller
We are not always able to validate input data with Attributes. For example, if you want to ask an external service about the correctness of data or database. There's nothing stopping you from returning an object instance ProblemDetails directly in the controller. You should remember then the responsibility for returning the appropriate error code and completing the OPEN API documentation rests with us.
Example
// POST: api/Todo
[HttpPost]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ValidationProblemDetails))]
public ActionResult Post([FromBody] TodoItem value)
{
if (!value.Title.Contains("A")) {
ModelState.AddModelError("Title", "String don't contain A letter");
return ValidationProblem();
}
return Ok();
}
Summary
As you can see above in ASP, error returning has been simplified to a minimum. In most cases, the programmer only needs to remember to add appropriate attributes to the input classes. Often, if we inherit model classes with database models from the Entity Framework, most of the attributes are already added at the database design stage.
The matter begins to get more complicated when we expect specific validation, but also when appropriate abstract classes have been prepared to implement our own logic.