How can I allow multiple domains in a .Net Web API with OAuth token authentication using CORS? - oauth

We have a .Net Framework Web API, with Token based OAuth authentication, and are trying to make a call to it via an Exchange HTML Add-In. I wish to allow access to several domains, as we may be using several different apps to access it, but we do not wish to allow general (*) access, as it is a proprietary web API, so there is no need for it to be accessed beyond known domains.
I have tried the following in order to satisfy the pre-flight:
Add the Access-Control-Allow-Origin headers with multiple domains via <system.webServer> - this returns a "header contains multiple values" CORS error when including multiple domains
Adding the Access-Control-Allow-Origin headers with multiple domains via a PreflightRequestsHandler : Delegating Handler - same result
If I set these up with one domain, and used the config.EnableCors with an EnableCorsAttribute with the domains, it would add those on to the headers and give an error with redundant domains.
How can I set up my Web API with OAuth and CORS settings for multiple domains?

You can add the header "Access-Control-Allow-Origin" in the response
of authorized sites in Global.asax file
using System.Linq;
private readonly string[] authorizedSites = new string[]
{
"https://site1.com",
"https://site2.com"
};
private void SetAccessControlAllowOrigin()
{
string origin = HttpContext.Current.Request.Headers.Get("Origin");
if (authorizedSites.Contains(origin))
HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", origin);
}
protected void Application_BeginRequest()
{
SetAccessControlAllowOrigin();
}

Found the following from Oscar Garcia (#ozkary) at https://www.ozkary.com/2016/04/web-api-owin-cors-handling-no-access.html, implemented it and it worked perfectly! Added to AppOAuthProvider which Microsoft had set up on project creation:
/// <summary>
/// match endpoint is called before Validate Client Authentication. we need
/// to allow the clients based on domain to enable requests
/// the header
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override Task MatchEndpoint(OAuthMatchEndpointContext context)
{
SetCORSPolicy(context.OwinContext);
if (context.Request.Method == "OPTIONS")
{
context.RequestCompleted();
return Task.FromResult(0);
}
return base.MatchEndpoint(context);
}
/// <summary>
/// add the allow-origin header only if the origin domain is found on the
/// allowedOrigin list
/// </summary>
/// <param name="context"></param>
private void SetCORSPolicy(IOwinContext context)
{
string allowedUrls = ConfigurationManager.AppSettings["allowedOrigins"];
if (!String.IsNullOrWhiteSpace(allowedUrls))
{
var list = allowedUrls.Split(',');
if (list.Length > 0)
{
string origin = context.Request.Headers.Get("Origin");
var found = list.Where(item => item == origin).Any();
if (found){
context.Response.Headers.Add("Access-Control-Allow-Origin",
new string[] { origin });
}
}
}
context.Response.Headers.Add("Access-Control-Allow-Headers",
new string[] {"Authorization", "Content-Type" });
context.Response.Headers.Add("Access-Control-Allow-Methods",
new string[] {"OPTIONS", "POST" });
}

Related

Microsoft Graph API - Manage Booking API Authorization has been denied for this request

I have a simple ASP.Net web application consist of .aspx web from hosted on azure as cloud service. In my application there is no user login.
I want to connect with Microsoft Graph API and and to use Microsoft Bookings API to get the BookingBusiness collection on my home page load without user login. I am currently debugging my web app on my desktop using Azure emulator.
I have the ofiice 365 premium account access assoiciated with my microsoft account (v-sheeal#microsoft.com) and I had created a Booking business using my v- alias through Booking tools (https://outlook.office.com/owa/?path=/bookings).
I registered an app in AAD in the same tenant with all required permission and provided the Cliend Id and secret in the code to get the access token. I am using Client credentials Grant flow to get the access token and try to invoke the booking API. I am able to get the access token, but when the code try to get the the list of booking businesses it is giving below exception.
DataServiceClientException: {
"error": {
"code": "",
"message": "Authorization has been denied for this request.",
"innerError": {
"request-id": "d0ac6470-9aae-4cc2-9bf3-ac83e700fd6a",
"date": "2018-09-03T08:38:29"
}
}
}
The code and registered app setting details are in below screen shot.
.aspx.cs
private static async Task<AuthenticationResult> AcquireToken()
{
var tenant = "microsoft.onmicrosoft.com";
//"yourtenant.onmicrosoft.com";
var resource = "https://graph.microsoft.com/";
var instance = "https://login.microsoftonline.com/";
var clientID = "7389d0b8-1611-4ef9-a01f-eba4c59a6427";
var secret = "mxbPBS10|[#!mangJHQF791";
var authority = $"{instance}{tenant}";
var authContext = new AuthenticationContext(authority);
var credentials = new ClientCredential(clientID, secret);
var authResult = await authContext.AcquireTokenAsync(resource,
credentials);
return authResult;
}
protected void MSBooking()
{
var authenticationContext = new
AuthenticationContext(GraphService.DefaultAadInstance,
TokenCache.DefaultShared);
var authenticationResult = AcquireToken().Result;
var graphService = new GraphService(
GraphService.ServiceRoot,
() => authenticationResult.CreateAuthorizationHeader());
// Get the list of booking businesses that the logged on user can see.
var bookingBusinesses = graphService.BookingBusinesses; ----- this
line throwing an exception "Authorization has been denied for
this request."
}
GraphService.cs
namespace Microsoft.Bookings.Client
{
using System;
using System.Net;
using Microsoft.OData;
using Microsoft.OData.Client;
public partial class GraphService
{
/// <summary>
/// The resource identifier for the Graph API.
/// </summary>
public const string ResourceId = "https://graph.microsoft.com/";
/// <summary>
/// The default AAD instance to use when authenticating.
/// </summary>
public const string DefaultAadInstance =
"https://login.microsoftonline.com/common/";
/// <summary>
/// The default v1 service root
/// </summary>
public static readonly Uri ServiceRoot = new
Uri("https://graph.microsoft.com/beta/");
/// <summary>
/// Initializes a new instance of the <see
cref="BookingsContainer"/> class.
/// </summary>
/// <param name="serviceRoot">The service root.</param>
/// <param name="getAuthenticationHeader">A delegate that returns
the authentication header to use in each request.</param>
public GraphService(Uri serviceRoot, Func<string>
getAuthenticationHeader)
: this(serviceRoot)
{
this.BuildingRequest += (s, e) => e.Headers.Add("Authorization",
getAuthenticationHeader());
}
}
According to your description, I assume you want to use the Microsoft Bookings API.
Base on the images you’ve provided, You are missing define scope in your code and the Authority is incorrectly.
We can review document to get an Access Token without a user.

ASP.MVC custom routing from DB [duplicate]

This question already has an answer here:
Multiple levels in MVC custom routing
(1 answer)
Closed 7 years ago.
I have an Asp.Net MVC site with a MS SQL database. This site has an administration panel where, apart from other things, the administrator can change the menu of the site.
What we want to do is allow the owner of the site to change dynamically not only the menu names but also the page routes, so they can decide the url of any page in the site.
Imagine that we have different pages(views) like videos, news, photos...the default routes (url) for those view can be:
www.site.com/videos
www.site.com/news
www.site.com/photos
The admin has to be able to change dynamically those routes son when a user hit the news page it shows the URL they want, for example:
www.site.com/my-videos
www.site.com/latest-news
www.site.com/photo-gallery
The idea is loading the site menu from DB, getting the name of the menu, the controller, the action and the route of the page. And from there we have to call a controller and action to load a view but we need to show in the URL the route the admin has set for that view.
Also it is possible that we have multiple actions(views) in the same controller. For example news and videos are in the same controller.
If we pass a parameter "customRoute" to the Route.Config it gives us an error because the name of that parameter is the same for those actions in the same controller.
How can we do this with the ASP.NET routing?
Thanks in advance.
The code below shows how to add routes from a database to your route config (this only gets executed when the application pool starts)
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("favicon.ico");
var redirects = LegacyRedirectRepo.GetRedirects();
foreach (var legacyRedirect in redirects)
{
if (!legacyRedirect.Source.Contains("?"))
{
routes.Add(new LegacyRoute(legacyRedirect.Source, legacyRedirect.Destination));
}
}
routes.IgnoreRoute("{folder}/{*pathInfo}", new { folder = "upload" });
routes.IgnoreRoute("{folder}/{*pathInfo}", new { folder = "content" });
routes.IgnoreRoute(
"{*staticfile}",
new { staticfile = #".*\.(jpg|gif|jpeg|png|js|css|htm|html)$" }
);
//static routing rules
}
Or you could override the BeginProcessRequest with something like this
public class LegacyHandler : MvcHandler
{
/// <summary>
/// Initializes a new instance of the <see cref="LegacyHandler"/> class.
/// </summary>
/// <param name="requestContext">The request context.</param>
public LegacyHandler(RequestContext requestContext)
: base(requestContext)
{
}
/// <summary>
/// Called by ASP.NET to begin asynchronous request processing.
/// </summary>
/// <param name="httpContext">The HTTP context.</param>
/// <param name="callback">The asynchronous callback method.</param>
/// <param name="state">The state of the asynchronous object.</param>
/// <returns>The status of the asynchronous call.</returns>
protected override System.IAsyncResult BeginProcessRequest(HttpContext httpContext, System.AsyncCallback callback, object state)
{
var legacyRoute = RequestContext.RouteData.Route as LegacyRoute;
httpContext.Response.Status = "301 Moved Permanently";
var urlBase = RequestContext.HttpContext.Request.Url.GetLeftPart(System.UriPartial.Authority);
var url = string.Format("{0}/{1}", urlBase, legacyRoute.Target);
if (!string.IsNullOrWhiteSpace(RequestContext.HttpContext.Request.Url.Query))
{
var pathAndQuery = RequestContext.HttpContext.Request.Url.PathAndQuery;
pathAndQuery = pathAndQuery.Substring(1, pathAndQuery.Length - 1);
var redirect = LegacyRedirectRepo.GetRedirect(pathAndQuery);
url = string.Format(#"{0}/{1}", urlBase, redirect.Destination);
}
httpContext.Response.RedirectPermanent(url);
httpContext.Response.End();
return null;
}
}

How is Owin able to set the Asp.Net Identity authentication cookies after the Application_EndRequest stage?

As a test, I created a fresh Asp.Net MVC5 app using the latest template in Visual Studio 2013. I added the following method to Global.asax.cs:
protected void Application_PreSendRequestHeaders()
{
Response.AppendCookie(new HttpCookie("TotalNumberOfCookiesInApplication_EndRequestIs", Response.Cookies.Count + string.Empty));
}
When I start the app and do a POST to /Account/Login using the credentials of a registered user, the cookies that get returned to the client are:
Note that the custom cookie I've added shows that there are no cookies set in the response by the time Application_PreSendRequestHeaders() is called. Despite this, all the Auth cookies arrive at the client. I was of the understanding that Application_PreSendRequestHeaders() is the last stage we can "hook" into for modifying cookies. Is the Owin middleware able to somehow add cookies after that, or am I missing something?
(In case you're interested, my motivation for all this is: I'm trying to modify the domain of the auth cookies to be ".abc.com", where "abc.com" is the last two parts of the host in the request URI. I want to do this to support authentication across multiple subdomains. Setting the CookieDomain in the context of the global Owin configuration (IAppBuilder) isn't enough because the request host changes between our debug/staging/production environments, and we often deploy the production code to Azure staging first for testing before doing a VIP swap).
(Note also that I'm aware of posts like this one, however it doesn't explain where the cookies are actually set)
EDIT:
Based on a bit more searching, it seems I'm looking into the wrong pipeline. Owin has its own pipeline, so I found this post which describes how we can hook into it. Viola...there were the cookies. Would be great if anybody could confirm that this is indeed the most sensible way to do it.
EDIT 2:
Finally decided to look into the Katana source code and found out that all I needed to do to get my cookie domains set was the following code in my CookieAuthenticationProvider
OnResponseSignIn = context =>
{
// Example only!
context.CookieOptions.Domain = context.Request.Uri.Host;
},
OnResponseSignOut = context =>
{
// Example only!
context.CookieOptions.Domain = context.Request.Uri.Host;
}
Edit 3:
An even cleaner solution for my case was just to use a custom cookie manager, which set the cookie domain based on the current request URI:
/// <summary>
/// This class simply appends the cookie domain to the usual auth cookies
/// </summary>
public class ChunkingCookieManagerWithSubdomains : ICookieManager
{
private readonly ChunkingCookieManager _chunkingCookieManager;
public ChunkingCookieManagerWithSubdomains()
{
_chunkingCookieManager = new ChunkingCookieManager();
}
public string GetRequestCookie(IOwinContext context, string key)
{
return _chunkingCookieManager.GetRequestCookie(context, key);
}
public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options)
{
// Simplification (use the context parameter to get the required request info)
options.Domain = ".domainBasedOnRequestInContext.com";
_chunkingCookieManager.AppendResponseCookie(context, key, value, options);
}
public void DeleteCookie(IOwinContext context, string key, CookieOptions options)
{
// Simplification (use the context parameter to get the required request info)
options.Domain = ".domainBasedOnRequestInContext.com";
_chunkingCookieManager.DeleteCookie(context, key, options);
}
}
...which is then set in the Cookie Auth Options in the Owin setup:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
...
CookieManager = new ChunkingCookieManagerWithSubdomains(),
...
}
});
Hope that helps somebody coming across the same kind of question.
As requested by Tieson, here's a summary of my edits in the original post above, as an answer.
Suggested solution: Use a custom cookie manager.
/// <summary>
/// This class simply appends the cookie domain to the usual auth cookies
/// </summary>
public class ChunkingCookieManagerWithSubdomains : ICookieManager
{
private readonly ChunkingCookieManager _chunkingCookieManager;
public ChunkingCookieManagerWithSubdomains()
{
_chunkingCookieManager = new ChunkingCookieManager();
}
public string GetRequestCookie(IOwinContext context, string key)
{
return _chunkingCookieManager.GetRequestCookie(context, key);
}
public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options)
{
// Simplification (use the context parameter to get the required request info)
options.Domain = ".domainBasedOnRequestInContext.com";
_chunkingCookieManager.AppendResponseCookie(context, key, value, options);
}
public void DeleteCookie(IOwinContext context, string key, CookieOptions options)
{
// Simplification (use the context parameter to get the required request info)
options.Domain = ".domainBasedOnRequestInContext.com";
_chunkingCookieManager.DeleteCookie(context, key, options);
}
}
...which can then be set in the Cookie Auth Options in the Owin setup:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
...
CookieManager = new ChunkingCookieManagerWithSubdomains(),
...
}
});

OData request show showing up in Fiddler (SharePoint & Silverlight 5)

I've got a Silverlight 5 app that is calling an OData service (the OOTB one incuded with SharePoint 2010) to pull data back form a list. The site is secured using Windows Authentication. When I run my test I get prompted to login but the results always say there are zero results returned in the result set.
Now here's what's strange. I know there's data in the list (and when I manually plug in the OData request URL, I see results come back in the browser). When I watch Fiddler while running the test, I see a few requests for clientaccesspolicy.xml (all result in a 401 response)... then I login & it successfully obtains the clientaccesspolicy.xml file. However, even though the app says it ran the query and got zero results back, I don't see the actual OData service request in Fiddler (nothing after the successful call to clientaccesspolicy.xml.
Here's what the code looks like:
private DataServiceCollection<InstructorsItem> _dataCollection = new DataServiceCollection<InstructorsItem>();
private Action<IEnumerable<Instructor>> _callbackWithData;
/// <summary>
/// Retrieves a list of instructors from the data service.
/// </summary>
public void GetInstructors(Action<IEnumerable<Instructor>> callback) {
// save callbacks
ResetCallbacks();
_callbackWithData = callback;
// get the instructors
var query = from instructor in IntranetContext.Instructors
select instructor;
// execute query
RunQuery(query);
}
/// <summary>
/// Retrieves instructors from the data source based on the specified query.
/// </summary>
/// <param name="query">Query to execute</param>
private void RunQuery(IQueryable<InstructorsItem> query) {
// clear the collection & register the load completed method
_dataCollection.Clear();
_dataCollection.LoadCompleted += OnLoadDataCompleted;
// fire the load
_dataCollection.LoadAsync(query.Take(5));
}
/// <summary>
/// Handler when the data has been loaded from the service.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void OnLoadDataCompleted(object sender, LoadCompletedEventArgs e) {
// remove the event handler preventing double loads
_dataCollection.LoadCompleted -= OnLoadDataCompleted;
// convert the data to a generic list of objects
var results = _dataCollection.ToList<InstructorsItem>();
// TODO: convert results to local objects
List<Instructor> convertedResults = new List<Instructor>();
foreach (var item in results) {
convertedResults.Add(new Instructor() {
SharePointId = item.Id,
Name = item.Title
});
}
// run the callback
_callbackWithData(convertedResults);
}
And here's what the test runner looks like that's triggering it:
[TestMethod]
[Asynchronous]
[Description("Test loading instructors from the OData Intranet service.")]
public void TestGetInstructors() {
bool asyncCallCompleted = false;
List<Instructor> result = null;
// call data service
_dataService.GetInstructors(asyncResult => {
asyncCallCompleted = true;
result = new List<Instructor>(asyncResult);
});
// run test when call completed
EnqueueConditional(() => asyncCallCompleted);
EnqueueCallback(
() => Assert.IsTrue(result.Count > 0, "Didn't retrieve any instructors."));
EnqueueTestComplete();
}
Can't for the life of me figure out (1) why i'm not seeing the query showing up in Fiddler when it is saying there are no errors, in fact it says there are zero errors when running the test.
If you are running the server and client on the same machine, there is no external HTTP traffic so there is nothing for Fiddler to pick up.

Best way to implement request throttling in ASP.NET MVC?

We're experimenting with various ways to throttle user actions in a given time period:
Limit question/answer posts
Limit edits
Limit feed retrievals
For the time being, we're using the Cache to simply insert a record of user activity - if that record exists if/when the user does the same activity, we throttle.
Using the Cache automatically gives us stale data cleaning and sliding activity windows of users, but how it will scale could be a problem.
What are some other ways of ensuring that requests/user actions can be effectively throttled (emphasis on stability)?
Here's a generic version of what we've been using on Stack Overflow for the past year:
/// <summary>
/// Decorates any MVC route that needs to have client requests limited by time.
/// </summary>
/// <remarks>
/// Uses the current System.Web.Caching.Cache to store each client request to the decorated route.
/// </remarks>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class ThrottleAttribute : ActionFilterAttribute
{
/// <summary>
/// A unique name for this Throttle.
/// </summary>
/// <remarks>
/// We'll be inserting a Cache record based on this name and client IP, e.g. "Name-192.168.0.1"
/// </remarks>
public string Name { get; set; }
/// <summary>
/// The number of seconds clients must wait before executing this decorated route again.
/// </summary>
public int Seconds { get; set; }
/// <summary>
/// A text message that will be sent to the client upon throttling. You can include the token {n} to
/// show this.Seconds in the message, e.g. "Wait {n} seconds before trying again".
/// </summary>
public string Message { get; set; }
public override void OnActionExecuting(ActionExecutingContext c)
{
var key = string.Concat(Name, "-", c.HttpContext.Request.UserHostAddress);
var allowExecute = false;
if (HttpRuntime.Cache[key] == null)
{
HttpRuntime.Cache.Add(key,
true, // is this the smallest data we can have?
null, // no dependencies
DateTime.Now.AddSeconds(Seconds), // absolute expiration
Cache.NoSlidingExpiration,
CacheItemPriority.Low,
null); // no callback
allowExecute = true;
}
if (!allowExecute)
{
if (String.IsNullOrEmpty(Message))
Message = "You may only perform this action every {n} seconds.";
c.Result = new ContentResult { Content = Message.Replace("{n}", Seconds.ToString()) };
// see 409 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
c.HttpContext.Response.StatusCode = (int)HttpStatusCode.Conflict;
}
}
}
Sample usage:
[Throttle(Name="TestThrottle", Message = "You must wait {n} seconds before accessing this url again.", Seconds = 5)]
public ActionResult TestThrottle()
{
return Content("TestThrottle executed");
}
The ASP.NET Cache works like a champ here - by using it, you get automatic clean-up of your throttle entries. And with our growing traffic, we're not seeing that this is an issue on the server.
Feel free to give feedback on this method; when we make Stack Overflow better, you get your Ewok fix even faster :)
Microsoft has a new extension for IIS 7 called Dynamic IP Restrictions Extension for IIS 7.0 - Beta.
"The Dynamic IP Restrictions for IIS 7.0 is a module that provides protection against denial of service and brute force attacks on web server and web sites. Such protection is provided by temporarily blocking IP addresses of the HTTP clients who make unusually high number of concurrent requests or who make large number of requests over small period of time."
http://learn.iis.net/page.aspx/548/using-dynamic-ip-restrictions/
Example:
If you set the criteria to block after X requests in Y milliseconds or X concurrent connections in Y milliseconds the IP address will be blocked for Y milliseconds then requests will be permitted again.
We use the technique borrowed from this URL http://www.codeproject.com/KB/aspnet/10ASPNetPerformance.aspx, not for throttling, but for a poor man's Denial Of Service (D.O.S). This is also cache-based, and may be similar to what you are doing. Are you throttling to prevent D.O.S. attacks? Routers can certainly be used to reduce D.O.S; do you think a router could handle the throttling you need?
It took me some time to work out an equivalent for .NET 5+ (formerly .NET Core), so here's a starting point.
The old way of caching has gone and been replaced by Microsoft.Extensions.Caching.Memory with IMemoryCache.
I separated it out a bit more, so here's what you need...
The Cache Management Class
I've added the whole thing here, so you can see the using statements.
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Primitives;
using System;
using System.Threading;
namespace MyWebApplication
{
public interface IThrottleCache
{
bool AddToCache(string key, int expriryTimeInSeconds);
bool AddToCache<T>(string key, T value, int expriryTimeInSeconds);
T GetFromCache<T>(string key);
bool IsInCache(string key);
}
/// <summary>
/// A caching class, based on the docs
/// https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-6.0
/// Uses the recommended library "Microsoft.Extensions.Caching.Memory"
/// </summary>
public class ThrottleCache : IThrottleCache
{
private IMemoryCache _memoryCache;
public ThrottleCache(IMemoryCache memoryCache)
{
_memoryCache = memoryCache;
}
public bool AddToCache(string key, int expriryTimeInSeconds)
{
bool isSuccess = false; // Only a success if a new value gets added.
if (!IsInCache(key))
{
var cancellationTokenSource = new CancellationTokenSource(
TimeSpan.FromSeconds(expriryTimeInSeconds));
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSize(1)
.AddExpirationToken(
new CancellationChangeToken(cancellationTokenSource.Token));
_memoryCache.Set(key, DateTime.Now, cacheEntryOptions);
isSuccess = true;
}
return isSuccess;
}
public bool AddToCache<T>(string key, T value, int expriryTimeInSeconds)
{
bool isSuccess = false;
if (!IsInCache(key))
{
var cancellationTokenSource = new CancellationTokenSource(
TimeSpan.FromSeconds(expriryTimeInSeconds));
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(DateTimeOffset.Now.AddSeconds(expriryTimeInSeconds))
.SetSize(1)
.AddExpirationToken(
new CancellationChangeToken(cancellationTokenSource.Token));
_memoryCache.Set<T>(key, value, cacheEntryOptions);
isSuccess = true;
}
return isSuccess;
}
public T GetFromCache<T>(string key)
{
return _memoryCache.Get<T>(key);
}
public bool IsInCache(string key)
{
var item = _memoryCache.Get(key);
return item != null;
}
}
}
The attribute itself
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Net;
namespace MyWebApplication
{
/// <summary>
/// Decorates any MVC route that needs to have client requests limited by time.
/// Based on how they throttle at stack overflow (updated for .NET5+)
/// https://stackoverflow.com/questions/33969/best-way-to-implement-request-throttling-in-asp-net-mvc/1318059#1318059
/// </summary>
/// <remarks>
/// Uses the current System.Web.Caching.Cache to store each client request to the decorated route.
/// </remarks>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class ThrottleByIPAddressAttribute : ActionFilterAttribute
{
/// <summary>
/// The caching class (which will be instantiated as a singleton)
/// </summary>
private IThrottleCache _throttleCache;
/// <summary>
/// A unique name for this Throttle.
/// </summary>
/// <remarks>
/// We'll be inserting a Cache record based on this name and client IP, e.g. "Name-192.168.0.1"
/// </remarks>
public string Name { get; set; }
/// <summary>
/// The number of seconds clients must wait before executing this decorated route again.
/// </summary>
public int Seconds { get; set; }
/// <summary>
/// A text message that will be sent to the client upon throttling. You can include the token {n} to
/// show this.Seconds in the message, e.g. "Wait {n} seconds before trying again".
/// </summary>
public string Message { get; set; } = "You may only perform this action every {n} seconds.";
public override void OnActionExecuting(ActionExecutingContext c)
{
if(_throttleCache == null)
{
var cache = c.HttpContext.RequestServices.GetService(typeof(IThrottleCache));
_throttleCache = (IThrottleCache)cache;
}
var key = string.Concat(Name, "-", c.HttpContext.Request.HttpContext.Connection.RemoteIpAddress);
var allowExecute = _throttleCache.AddToCache(key, Seconds);
if (!allowExecute)
{
if (String.IsNullOrEmpty(Message))
Message = "You may only perform this action every {n} seconds.";
c.Result = new ContentResult { Content = Message.Replace("{n}", Seconds.ToString()) };
// see 409 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
c.HttpContext.Response.StatusCode = (int)HttpStatusCode.Conflict;
}
}
}
}
Startup.cs or Program.cs - Register the services with DI
This example uses Startup.cs/ConfigureServices - Put the code somewhere after AddControllersWithViews).
For a project created in .NET6+ I think you'd add the equivalent between builder.Services.AddRazorPages(); and var app = builder.Build(); in program.cs. services would be builder.Services.
If you don't get the placement of this code right, the cache will be empty every time you check it.
// The cache for throttling must be a singleton and requires IMemoryCache to be set up.
// Place it after AddControllersWithViews or AddRazorPages as they build a cache themselves
// Need this for IThrottleCache to work.
services.AddMemoryCache(_ => new MemoryCacheOptions
{
SizeLimit = 1024, /* TODO: CHECK THIS IS THIS THE RIGHT SIZE FOR YOU! */
CompactionPercentage = .3,
ExpirationScanFrequency = TimeSpan.FromSeconds(30),
});
services.AddSingleton<IThrottleCache, ThrottleCache>();
Example Usage
[HttpGet, Route("GetTest")]
[ThrottleByIPAddress(Name = "MyControllerGetTest", Seconds = 5)]
public async Task<ActionResult<string>> GetTest()
{
return "Hello world";
}
To help understand caching in .NET 5+, I've also made a caching console demo.
Since the highly voted answers to this question are too old, I am sharing the latest solution which worked for me.
I tried using the Dynamic IP restrictions as given in an answer on this page but when I tried to use that extension, I found that this extension has been discontinued by Microsoft and on the download page they have clearly written the below message.
Microsoft has discontinued the Dynamic IP Restrictions extension and this download is no longer available.
So I researched further and found that the Dynamic IP Restrictions is now by default included in IIS 8.0 and above. The below information is fetched from the Microsoft Dynamic IP Restrictions page.
In IIS 8.0, Microsoft has expanded the built-in functionality to include several new features:
Dynamic IP address filtering, which allows administrators to
configure their server to block access for IP addresses that exceed
the specified number of requests.
The IP address filtering features now allow administrators to specify
the behavior when IIS blocks an IP address, so requests from
malicious clients can be aborted by the server instead of returning
HTTP 403.6 responses to the client.
IP filtering now feature a proxy mode, which allows IP addresses to
be blocked not only by the client IP that is seen by IIS but also by
the values that are received in the x-forwarded-for HTTP header
For step by step instructions to implement Dynamic IP Restrictions, please visit the below link:
https://learn.microsoft.com/en-us/iis/get-started/whats-new-in-iis-8/iis-80-dynamic-ip-address-restrictions
I hope it helps someone stuck in a similar problem.
Created ThrottlingTroll - my take on throttling/rate limiting in ASP.NET Core.
It is similar to Stefan Prodan's AspNetCoreRateLimit and ASP.NET 7's Rate Limiting Middleware, but has advantages:
Both ingress and egress throttling (egress means that your specially configured HttpClient won't make more than N requests per second and will instead produce 429 status code by itself).
Distributed rate counter stores (including, but not limited to Redis).
Dynamic (re)configuration - allows to adjust limits without restarting the service.
Propagating 429 statuses from egress to ingress.
Check out more in the repo.

Resources