Disabling Controller methods in ASP.NET Core

Posted by ryansouthgate on 14 Feb 2023

In this post I’m going to demonstrate how to globally disable (certain) controller Methods using ASP.NET Core.

Why

I’ve been coding a Stripe integration recently. I’ve implemented WebHooks to allow Stripe to notify me of any events that happen on their end (e.g. payments that succeed/fail, coupons created etc). The WebHook calls into a specific method in an ASP.NET Controller. POST-ing a JSON body, which is then verified for authenticity and deserialised into C# objects. From these objects I then save information in our Database and send out emails to customers. Stripe have a CLI which allows you to test these WebHooks locally. I have a separate “Development-Only” WebHook endpoint in an ASP.NET Core controller. This is useful when running the solution locally, and I want this to work whenever I’m developing on my local machine. However as this WebHook endpoint skips the Stripe’s supplied signature verification, I don’t want this endpoint to be available when this code gets deployed to our Stage/Production environments.

How

I’ve created a DevelopmentOnlyAttribute, that I can apply to any controller endpoints, which I only want accessible for development purposes. I just pop the [DevelopmentOnly] attribute, on the endpoint I don’t want exposing in different environments, and voilĂ  that endpoint will return a 404 (Not Found)

ASP.NET Core Filters allow you to run code before/after controller methods and allow you to short-circuit requests, so (in our case), the request never even makes it to the controller. We back out as soon as we realise the endpoint is not to be called in this environment.

Here’s the custom Filter Attribute, I’ve commented the code in-line to walk you through what it’s doing

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace mywebapp.Filters
{
    public class DevelopmentOnlyAttribute : IResourceFilter
    {
        public void OnResourceExecuted(ResourceExecutedContext context)
        {
            // Nothing in here, as this is run after our Controller (Endpoint) method finishes
        }

        public void OnResourceExecuting(ResourceExecutingContext context)
        {
            // Get an object (from DI), which contains information about our current WebHost Environment
            var env = context.HttpContext.RequestServices.GetService<IWebHostEnvironment>();
            if (!env.IsDevelopment())
            {
                // If we're not running in development mode, then just return a 404, this short-circuits the request pipeline. (This could be changed to return whatever fail result you like)
                context.Result = new NotFoundResult();
            }
        }
    }
}

Now it’s as simple as putting this attribute on to our controller methods. Below is an example of it protecting my StripeDevController

namespace mywebapp.api.webhooks.Controllers
{
    [Route("[controller]")]
    public class StripeDevController : Controller
    {
        private readonly IStripeService _stripeService;

        public StripeDevController(IStripeService stripeService)
        {
            _stripeService = stripeService;
        }

        [HttpPost("dev-process-webhook")]
        [AllowAnonymous]
        [DevelopmentOnly]
        public async Task<IActionResult> Process_Webhook()
        {
            using (var reader = new StreamReader(HttpContext.Request.Body))
            {
                var json = await reader.ReadToEndAsync();

                await _stripeService.ProcessWebhookEvent(json, Request.Headers["Stripe-Signature"]);

                return Ok();
            }
        }

    }
}

Further protection

For the super-cautious amoung you (me included). You can go one step further and use C# Preprocessor Directives and conditionally control compilation for our controller endpoints. The code below (same as above, with 2 conditional compile directives added) will ensure that our endpoint will not be compiled when you are building/publishing in Release configuration. Which means that this “development only” code, never makes it into your binaries, for your live system

namespace mywebapp.api.webhooks.Controllers
{
    [Route("[controller]")]
    public class StripeDevController : Controller
    {
        private readonly IStripeService _stripeService;

        public StripeDevController(IStripeService stripeService)
        {
            _stripeService = stripeService;
        }

#if DEBUG
        [HttpPost("dev-process-webhook")]
        [AllowAnonymous]
        [DevelopmentOnly]
        public async Task<IActionResult> Process_Webhook()
        {
            using (var reader = new StreamReader(HttpContext.Request.Body))
            {
                var json = await reader.ReadToEndAsync();

                await _stripeService.ProcessWebhookEvent(json, Request.Headers["Stripe-Signature"]);

                return Ok();
            }
        }
#endif
    }
}

Closing

All Done! You can rest safe in the knowledge that you’re not exposing test endpoints in your Production environments. You can use one or both of these features (to be super secure) to ensure you’re not exposing test/dev endpoints.

Thanks for reading and if you’ve done something similar, I’d love to know how you’ve tackled it!



comments powered by Disqus