I've created a web api application, to expose an ODATA API, to a front-end application. One of the reasons for doing this was to be able to return different content types, such as Excel files, for the same data.
I've made use of a custom Media Formatter to output my Excel data, however, I've noticed that when I call it, from the client, there is no security in place.
When making a GET, with no ACCEPT header, then the OAuth bearer token is checked and access is either accepted or revoked. The Authorization is set via [Authorize] on the controller.
When I make the same GET, with the ACCEPT header set to request an Excel file, the controller is called regardless of the token, bypassing the security on the controller.
I've obviously done something wrong, however, I can't work out what it could be. It's the same controller but for some reason, it's always allowing access when ACCEPT is set to a supported media type.
A cut-down version of my set up is below.
Owin Startup:
[assembly: OwinStartup(typeof(Rest.Startup))]
namespace Rest
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureOAuth(app);
HttpConfiguration config = new HttpConfiguration();
WebApiConfig.Register(config);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
}
private void ConfigureOAuth(IAppBuilder app)
{
OAuthAuthorizationServerOptions oauthServerOptions = new OAuthAuthorizationServerOptions
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new SimpleAuthorisationServerProvider()
};
// Token generation
app.UseOAuthAuthorizationServer(oauthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
}
The call into WebApiConfig.Register()
namespace Rest
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
var json = config.Formatters.JsonFormatter;
json.SerializerSettings.PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.Objects;
config.Formatters.Remove(config.Formatters.XmlFormatter);
config.Formatters.Add(new ExcelSimpleFormatter());
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
// Configure CORS globally
var cors = new EnableCorsAttribute(
origins:"*",
headers:"*",
methods:"*");
config.EnableCors(cors);
}
}
}
My media formatter (code removed to save space):
namespace Rest.Formatters
{
public class ExcelSimpleFormatter : BufferedMediaTypeFormatter
{
public ExcelSimpleFormatter()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/excel"));
}
public override bool CanWriteType(Type type)
{
return true;
}
public override bool CanReadType(Type type)
{
return false;
}
public override void WriteToStream(Type type, object value, Stream writeStream, HttpContent content)
{
// This gets called regardless of authorization
}
}
}
An example / simplified controller:
namespace Rest.Controllers
{
[Authorize]
public class TestController : ApiController
{
private dbSDSContext db = new dbSDSContext();
// GET: api/Test
public IQueryable<test> GetTests()
{
return db.test;
}
// GET: api/Test/5
[ResponseType(typeof(test))]
public async Task<IHttpActionResult> GetTest(int id)
{
test test = await db.test.FindAsync(id);
if (test == null)
{
return NotFound();
}
return Ok(test);
}
// PUT: api/Test/5
[ResponseType(typeof(void))]
public async Task<IHttpActionResult> PutTest(int id, test test)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (id != test.testID)
{
return BadRequest();
}
db.Entry(test).State = EntityState.Modified;
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!TestExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return StatusCode(HttpStatusCode.NoContent);
}
// POST: api/Test
[ResponseType(typeof(test))]
public async Task<IHttpActionResult> PostTest(test test)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.test.Add(test);
await db.SaveChangesAsync();
return CreatedAtRoute("DefaultApi", new { id = test.testID}, test);
}
// DELETE: api/Test/5
[ResponseType(typeof(test))]
public async Task<IHttpActionResult> DeleteTest(int id)
{
test test = await db.test.FindAsync(id);
if (test == null)
{
return NotFound();
}
db.test.Remove(test);
await db.SaveChangesAsync();
return Ok(test);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
private bool TestExists(int id)
{
return db.test.Count(e => e.testID == id) > 0;
}
}
}
The error was caused by using the wrong namespace in the controllers affected.
When using WebAPI ensure to use:
using System.Web.Http;
and not:
using System.Web.Mvc;
Related
I want to call an ASP.NET Core 2.1.0 Web API with a controller's method.
I tried following but I get an error
Cannot GET /api/remote/NewAc/test1
Code:
[Route("api/remote/{action}")]
//[Route("api/[controller]")]
[ApiController]
public class RemoteController : ControllerBase
{
private readonly MyContext _context;
public RemoteValsController(MyContext context)
{ _context = context; }
[HttpGet]
public async Task<OkObjectResult> NewAc()
{
var r = await _context.TypeOfAccounts.AnyAsync();
return Ok(new { r = true });
}
[HttpGet("{id}")]
public async Task<OkObjectResult> NewAc([FromRoute] string AccountType)
{
var r = await _context.TypeOfAccounts.AnyAsync(o => o.AccountType.ToUpper() == AccountType.ToUpper());
return Ok(new { r = !r });
}
}
Startup.cs
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
I tried both [HttpPost] and [HttpGet] but in vain.
Re-check the defined routes for the controller.
[Route("api/remote/[action]")] //<-- NOTE Token replacement in route templates
[ApiController]
public class RemoteController : ControllerBase {
private readonly MyContext _context;
public RemoteController(MyContext context) {
_context = context;
}
//GET api/remote/NewAc
[HttpGet]
public async Task<IActionResult> NewAc() {
var r = await _context.TypeOfAccounts.AnyAsync();
return Ok(new { r = true });
}
//GET api/remote/NewAc/test1
[HttpGet("{accountType}")]
public async Task<IActionResult> NewAc(string accountType) {
var r = await _context.TypeOfAccounts.AnyAsync(o => o.AccountType.ToUpper() == accountType.ToUpper());
return Ok(new { r = !r });
}
}
Reference Routing to controller actions in ASP.NET Core
First, mapped routes and attribute routing are an either/or affair. If you have route attributes involved, the route definition in your Startup is not applicable at all.
Second, you can't just throw [FromRoute] in front a param and magically have it in the route. In fact, that attribute isn't necessary at all unless there's some ambiguity about where the param value is actually supposed to come from. If you want it from the route, then it needs to be part of your route template. Simply:
[HttpGet("{id}/{AccountType}")]
public async Task<OkObjectResult> NewAc(string AccountType)
{
var r = await _context.TypeOfAccounts.AnyAsync(o => o.AccountType.ToUpper() == AccountType.ToUpper());
return Ok(new { r = !r });
}
I am trying to use the streaming mode in an ASP.NET WebAPI action:
I setup Startup.cs with
public static void Configuration(IAppBuilder app)
{
if (HostingEnvironment.IsHosted)
{
GlobalConfiguration.Configure(config => Configuration(app, config));
GlobalConfiguration.Configuration.Services.Replace(typeof(IHostBufferPolicySelector), new CustomWebHostBufferPolicySelector());
}
else
{
var config = new HttpConfiguration();
Configuration(app, config);
}
}
The class handling the buffer / streaming mode
// Try the worst case, everything has to be streamed...
public class CustomWebHostBufferPolicySelector : WebHostBufferPolicySelector
{
// Check incoming requests and modify their buffer policy
public override bool UseBufferedInputStream(object hostContext)
{
return false;
}
// You could also change the response behaviour too...but for this example, we are not
// going to do anything here...
// I override this method just to demonstrate the availability of this method.
public override bool UseBufferedOutputStream(System.Net.Http.HttpResponseMessage response)
{
return base.UseBufferedOutputStream(response);
}
}
But in my WebAPI controller action the value is set Classic instead of None...
[Route("api/v1/presentations/{presentationId:int}/documents/{documentId}")]
public async Task<HttpResponseMessage> Post(int presentationId, string documentId)
{
try
{
var readEntityBodyMode = HttpContext.Current.Request.ReadEntityBodyMode;
// Classic... but why?
I have a C# dll to query data using Entity Framework. After that I use owin to create a rest api to reference the dll to get data. I have two samples: one is use a console app to access the data, and it's ok. The other is I put the rest api inside a Windows service, and access it. It returns empty. I don't know why.
C# dll:
ef dll:
public class DemoContext:DbContext
{
private static DemoContext _instance;
public DemoContext() : base("MyDemoConnection")
{
Database.SetInitializer(new MigrateDatabaseToLatestVersion<DemoContext, Configuration>());
}
public static DemoContext Instance
{
get
{
if (_instance == null)
{
_instance = new DemoContext();
}
return _instance;
}
}
public DbSet<Student> Students { get; set; }
public DbSet<Teacher> Teachers { get; set; }
}
owin api:
public class Startup
{
public void Configuration(IAppBuilder appBuilder)
{
// Configure Web API for self-host.
HttpConfiguration config = new HttpConfiguration();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
appBuilder.UseWebApi(config);
}
}
public class TeachersController : ApiController
{
[HttpGet]
[Route("api/teachers")]
public HttpResponseMessage Get()
{
try
{
var _context = DemoContext.Instance;
var teachers = _context.Teachers.ToList();
var response = Request.CreateResponse(HttpStatusCode.OK, teachers);
return response;
}
catch (Exception ex)
{
return Request.CreateResponse(HttpStatusCode.OK, ex.Message);
}
}
}
console app:
class Program
{
static void Main(string[] args)
{
string baseAddress = "http://localhost:9000/";
// Start OWIN host
using (WebApp.Start<Startup>(url: baseAddress))
{
// Create HttpCient and make a request to api/values
/* HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var response = client.GetAsync(baseAddress + "api/values").Result;
var result = response.Content.ReadAsStringAsync().Result;
Console.WriteLine(result);*/
Console.ReadLine();
}
}
}
windowservice:
public partial class HostOwin : ServiceBase
{
private IDisposable owin;
public HostOwin()
{
InitializeComponent();
}
protected override void OnStart(string[] args)
{
string baseAddress = "http://localhost:9000/";
owin = WebApp.Start<Startup>(url: baseAddress);
}
protected override void OnStop()
{
owin.Dispose();
}
}
I am running a unit test of my PostMyModel route. However, within PostMyModel() I used the line Validate<MyModel>(model) to revalidate my model after it is changed. I am using a test context, so as not to be dependent on the db for the unit tests. I have posted the test context and post method below:
Test Context
class TestAppContext : APIContextInterface
{
public DbSet<MyModel> MyModel { get; set; }
public TestAppContext()
{
this.MyModels = new TestMyModelDbSet();
}
public int SaveChanges(){
return 0;
}
public void MarkAsModified(Object item) {
}
public void Dispose() { }
}
Post Method
[Route(""), ResponseType(typeof(MyModel))]
public IHttpActionResult PostMyModel(MyModel model)
{
//Save model in DB
model.status = "Waiting";
ModelState.Clear();
Validate<MyModel>(model);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.MyModels.Add(model);
try
{
db.SaveChanges();
}
catch (DbUpdateException)
{
if (MyModelExists(model.id))
{
return Conflict();
}
else
{
throw;
}
}
return CreatedAtRoute("DisplayMyModel", new { id = model.id }, model);
}
When the Validate<MyModel>(model) line runs, I get the error :
System.InvalidOperationException: ApiController.Configuration must not be null.
How can I correct this?
In order for the Validate command to run, there must be mock HttpRequest associated with the controller. The code to do this is below. This will mock a default HttpRequest, which is fairly unused in this case, allowing the method to be unit tested.
HttpConfiguration configuration = new HttpConfiguration();
HttpRequestMessage request = new HttpRequestMessage();
controller.Request = request;
controller.Request.Properties["MS_HttpConfiguration"] = configuration;
All of my controllers look like this:
[HttpPut]
[Route("api/businessname")]
[Authorize]
public HttpResponseMessage UpdateBusinessName(BusinessNameDto model) {
if (!ModelState.IsValid)
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
try {
_userService.UpdateBusinessName(User.Identity.Name, model.BusinessName);
return Request.CreateResponse(HttpStatusCode.OK, new ApiResponseDto() {});
} catch (Exception e) {
// logging code
//return Request.CreateResponse(HttpStatusCode.InternalServerError, e);
return Request.CreateResponse(HttpStatusCode.OK, new ApiResponseDto() { Success = false, Error = "Something bad happened :(" });
}
}
There's a lot of repeated stuff across my controllers. Ideally I could just have this:
[HttpPut]
[Route("api/businessname")]
[Authorize]
public HttpResponseMessage UpdateBusinessName(BusinessNameDto model) {
_userService.UpdateBusinessName(User.Identity.Name, model.BusinessName);
return Request.CreateResponse(HttpStatusCode.OK, new ApiResponseDto() {});
}
And tell WebAPI to do all that other stuff with every controller... but I don't know if that's possible. How can I make that happen?
You ca do the following:
1) Create a validation filter so that your action method executes ONLY if the model state is valid. So that you don't need to check ModelState.IsValid anymore in your action methods.
public class ValidationActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
ModelStateDictionary modelState = actionContext.ModelState;
if (!modelState.IsValid)
{
actionContext.Response = actionContext.Request.CreateErrorResponse(
HttpStatusCode.BadRequest, modelState);
}
}
}
2) Create a exception handling filter which will catch any exception thrown by the action method, serialize it and create a HTTP BadRequest response message to the client. So that you don't have to use try catch anymore in your action methods.
public class HandleExceptionFilter : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
var responseMessage = new HttpResponseMessage(HttpStatusCode.BadRequest);
responseMessage.Content = new StringContent(context.Exception.Message);
context.Response = responseMessage;
}
}
You can register these filters in WebApiConfig.cs by adding the below lines to it
config.Filters.Add(new ValidationActionFilter());
config.Filters.Add(new HandleExceptionFilter());
UPDATE: To be more specific to SB2055's scenario, i am adding the below code
public class HandleExceptionFilter : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
var model = new ApiResponseDto() { Success = false, Error = context.Exception.Message })
context.Response = context.Request.CreateResponse(HttpStatusCode.OK,
model);
}
}
You can use ActionFilterAttributes. For example, to validate requests you can create a class similar to this:
public class ValidateRequestAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (!actionContext.ModelState.IsValid)
{
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)
}
}
}
You could then selectively apply the filter to your Web API actions by decorating them with the attribute:
[ValidateRequest]
[HttpPut]
[Route("api/businessname")]
[Authorize]
public HttpResponseMessage UpdateBusinessName(BusinessNameDto model) {
...
}
Or apply them to all your actions during Web API setup using HttpConfiguration.Filters:
config.Filters.Add(new ValidateRequestAttribute());