Search code examples
c#odataasp.net-web-api2

AuthorizeAttribute being bypassed in Web API when using media formatters


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;
        }
    }
}

Solution

  • 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;