I am struggling with getting the package AspNetCoreRateLimit version 4.0.2 to work when hosting a Blazor WebAsssembly project in Azure.
The solution has been developed in Visual studio 2022 and is made up of 5 individual projects, where one of them is a standard API project. It is based on the net6.0 framework. I have a startup.cs configuration and a RateLimitingMiddleware class used to setup the rate limits and the configuration.
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddRateLimiting();
services.AddHsts(options =>
{
options.Preload = true;
options.IncludeSubDomains = true;
options.MaxAge = TimeSpan.FromDays(365);
});
services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 10,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorNumbersToAdd: null);
});
}, ServiceLifetime.Transient, ServiceLifetime.Singleton);
services.AddAutoMapper(typeof(Startup));
....
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRateLimiting();
if (env.IsDevelopment())
{
app.UseWebAssemblyDebugging();
app.UseSwagger();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseHsts();
...
}
RateLimitingMiddleware.cs
internal static class RateLimitingMiddleware
{
internal static IServiceCollection AddRateLimiting(this IServiceCollection services)
{
// Used to store rate limit counters and ip rules
services.AddOptions();
services.AddMemoryCache();
services.Configure<ClientRateLimitOptions>(options =>
{
options.EnableEndpointRateLimiting = true;
//options.RealIpHeader = "X-Real-IP";
options.ClientIdHeader = "authorization";
//options.EndpointWhitelist = new List<string> { "get:/_framework/*", "get:/_content/*", "*:/lib/*", "*:/css/*", "*:/js/", "*:/appsettings.json", "*:/images/" };
options.GeneralRules = new List<RateLimitRule>
{
new RateLimitRule
{
Endpoint="*:/api/*",
Period = "15m",
Limit=30
},
new RateLimitRule
{
Endpoint="*:/api/*",
Period = "12h",
Limit=20000
},
new RateLimitRule
{
Endpoint="*:/api/*",
Period = "7d",
Limit=1000000
}
};
});
services.AddInMemoryRateLimiting();
// Inject Counter and Store Rules
services.AddSingleton<IClientPolicyStore, MemoryCacheClientPolicyStore>();
services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();
services.AddSingleton<IClientPolicyStore, DistributedCacheClientPolicyStore>();
services.AddSingleton<IRateLimitCounterStore, DistributedCacheRateLimitCounterStore>();
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
services.AddSingleton<IProcessingStrategy, AsyncKeyLockProcessingStrategy>();
//services.AddSingleton<IRateLimitConfiguration, CustomRateLimitConfiguration>();
//
// Return the services
return services;
}
internal static IApplicationBuilder UseRateLimiting(this IApplicationBuilder app)
{
var ipPolicyStore = app.ApplicationServices.GetRequiredService<IIpPolicyStore>();
ipPolicyStore.SeedAsync().GetAwaiter().GetResult();
var clientPolicyStore = app.ApplicationServices.GetRequiredService<IClientPolicyStore>();
clientPolicyStore.SeedAsync().GetAwaiter().GetResult();
app.UseClientRateLimiting();
app.UseIpRateLimiting();
return app;
}
}
With the above configuration the api calls are being limited when testing localhost and using Postman. However when the api is uploaded to our Azure environment the rules are not being implemented correctly.
As stated in the rule defined in the middelware I would like to use the authorization token to limit the number of count the number of requests but cannot get it to work.
I hope that someone has dealt with the same issue and can see where I am going wrong?
Thanks,
Problem solved. Wrong ratelimitheader. Should be CLIENT-IP.
Related
I have been struggling with this for few days so I am asking whether what I am trying to do is possible.
Basically I am experimenting things with signalR in Azure. I am using the latest version of SignalR NuGet.
I have a small Web App defining a simple signalR hub I want to access via a Azure SignalR Service.
I then have a couple of apps which communicate via hub in the signalR service.
I have various issues.
Getting the hub to load on the AZ SignalR Service. It does not work all the time. Complaining about Serverless/Default settings...
2023-02-16T14:05:25.034917571Z info: Microsoft.Azure.SignalR.Connections.Client.Internal.WebSocketsTransport[1]
2023-02-16T14:05:25.034960072Z Starting transport. Transfer mode: Binary. Url: 'wss://nmg-opus-common-inventory-dev-sigr.service.signalr.net/server/?hub=myhub&cid=9e64b380-b752-4175-8fd2-215a2b62139d'.
2023-02-16T14:05:25.057819457Z info: Microsoft.Azure.SignalR.Connections.Client.Internal.WebSocketsTransport[11]
2023-02-16T14:05:25.057859557Z WebSocket closed by the server. Close status NormalClosure.
2023-02-16T14:05:25.057865857Z info: Microsoft.Azure.SignalR.ServiceConnection[24]
2023-02-16T14:05:25.057870457Z Connection to service '(Primary)https://nmg-opus-common-inventory-dev-sigr.service.signalr.net(hub=MyHub)' handshake failed, probably caused by network instability or service restart. Will retry after the back off period. Error detail: Azure SignalR Service is in serverless mode, server connection is not allowed.. Id: 9e64b380-b752-4175-8fd2-215a2b62139d
2023-02-16T14:05:25.057875657Z info: Microsoft.Azure.SignalR.Connections.Client.Internal.WebSocketsTransport[6]
2023-02-16T14:05:25.057880257Z Transport is stopping.
2
On the client side, it seems that the connection succeeds if the service is set to Serverless mode.
Once connected, my call to join a group just hang and never returns. The connection happens after I click an element (div) on a blazor page.
So I am not sure why something so simple does not work.
here is my very simple hub :
public class MyHub : Hub
{
public async Task SendMessage(string groupName, string message)
{
await Group(groupName).SendAsync("ReceiveMessage", message);
}
public async Task JoinGroup(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
}
public async Task LeaveGroup(string groupName)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
}
public override async Task OnConnectedAsync()
{
await Clients.Client(Context.ConnectionId).SendAsync("Connected", Context.ConnectionId);
}
}
here is the classic startup method for the app defining hub
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddAzureAppConfiguration();
services.AddSignalR(e => { e.MaximumReceiveMessageSize = 10240000; });
string SignalRURLHub = Configuration.GetValue<string>("Resources:SignalR:Inventory:ConnectionString");
services.AddSignalR().AddAzureSignalR(SignalRURLHub);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAzureAppConfiguration();
app.UseRouting();
var SignalRHubName = $"/{Configuration.GetValue<string>("Resources:SignalR:HubName")}";
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<MyHub>(SignalRHubName);
});
}
}
Code from the client applications Initializing the connection and starting it.
protected HubConnection _connection;
private readonly string groupName;
protected ClientHandler(string connectionString, string hubName, string groupName, string userId = null)
{
var serviceUtils = new ServiceUtils(connectionString);
var url = GetClientUrl(serviceUtils.Endpoint, hubName);
_connection = new HubConnectionBuilder()
.WithUrl(url
, option =>
{
option.AccessTokenProvider = () =>
{
return Task.FromResult(serviceUtils.GenerateAccessToken(url, userId));
};
})
.WithAutomaticReconnect(new AlwaysRetryPolicy())
.ConfigureLogging(logging =>
{
logging.SetMinimumLevel(LogLevel.Trace);
logging.AddConsole();
})
.Build();
this.groupName = groupName;
_connection.Reconnected += JoinGroup;
_connection.On("Connected", new[] { typeof(string) }, OnInitialConnection);
}
private string GetClientUrl(string endpoint, string hubName)
{
return $"{endpoint}/client/?hub={hubName}";
}
private async Task JoinGroup(string contextId)
{
try
{
await _connection.InvokeAsync("JoinGroup", groupName); //JoinGroup is C# method name
}
catch (Exception e)
{
}
}
// Called separately after the ClientHandler object is created.
public async Task StartAsyncWithRetry(CancellationToken cancellationToken = default)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
await _connection.StartAsync()
.ContinueWith(res =>
{
_connection.InvokeAsync("JoinGroup", groupName);
});
if (_connection.State == HubConnectionState.Disconnected)
{
await Task.Delay(GetRandomDelayMilliseconds());
continue;
}
return;
}
catch (Exception e)
{
//throw;
//cancellationToken.
await Task.Delay(GetRandomDelayMilliseconds());
}
}
}
I have tried tweaking various setting in Azure, in my apps, changed my code from synch to async...
I must be missing something ... I've found lot of post but many of them were out of date. Also tried my luck with ChatGPT ...
I wrote a custom auth handler for a web API in .net core 3.0 following this tutorial by Jason Watmore. The Authorize attribute works great IIS express. However, when I publish the code to Azure Web App the Authorize attribute does not fire. There is no authentication challenge and data is returned without authentication.
Azure Authentication Authorization Settings
Here is the custom BasicAuthenticationHandler
public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly IAPIRepo _apiRepo;
public BasicAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder,
ISystemClock clock,
IAPIRepo apiRepo): base(options, logger, encoder, clock)
{
_apiRepo = apiRepo;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
//throw new NotImplementedException();
if (!Request.Headers.ContainsKey("Authorization"))
return AuthenticateResult.Fail("Missing Authorization Header");
User user = null;
try
{
var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
var credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] { ':' }, 2);
var username = credentials[0];
var password = credentials[1];
user = _apiRepo.Authenticate(username, password);
}
catch
{
return AuthenticateResult.Fail("Invalid Authorization Header");
}
if (user == null)
return AuthenticateResult.Fail("Invalid Username or Password");
var claims = new[] {
new Claim(ClaimTypes.NameIdentifier, user.User_Id.ToString()),
new Claim(ClaimTypes.Name, user.UserName),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
Startup.cs
services.AddScoped<IAPIRepo, APIRepo>();
services.AddAuthentication("BasicAuthentication")
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Edit: Difference between .net core 2.2 and 3.1. Changing the run time to 3.1 fixed the issue
It looks like you are using the Startup.cs of .NET Core 3.0 instead of 3.1 like the article is using.
I have enabled logging to Application logging to both FileSystem and Blob. I log messages using ILogger<T>.LogInformation() and Trace.WriteLine(). None of those are visible in blob. I also cannot find them on filesystem. I also can't see them when I enable log streaming.
Do I need to configure something in my ASP.NET Core application?
The [Microsoft.Extensions.Logging.AzureAppServices][1] provider package writes logs to text files in an Azure App Service app's file system and to blob storage in an Azure Storage account.
The provider package isn't included in the shared framework. To use the provider, add the provider package to the project.
To configure provider settings, use AzureFileLoggerOptions and AzureBlobLoggerOptions, as shown in the following example:
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
var todoRepository = host.Services.GetRequiredService<ITodoRepository>();
todoRepository.Add(new Core.Model.TodoItem() { Name = "Feed the dog" });
todoRepository.Add(new Core.Model.TodoItem() { Name = "Walk the dog" });
var logger = host.Services.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Seeded the database.");
host.Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureLogging(logging => logging.AddAzureWebAppDiagnostics())
.ConfigureServices(serviceCollection => serviceCollection
.Configure<AzureFileLoggerOptions>(options =>
{
options.FileName = "azure-diagnostics-";
options.FileSizeLimit = 50 * 1024;
options.RetainedFileCountLimit = 5;
}).Configure<AzureBlobLoggerOptions>(options =>
{
options.BlobName = "log.txt";
})
)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
You can refer below link for additional reference:
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-3.1#azure-app-service-provider
Hope it helps.
Here is a simple way(the referred article is here):
I tested it with .NET core 2.2 mvc project.
The necessary nuget package:
Microsoft.Extensions.Logging.AzureAppServices, version 2.2.0
1.In Startup.cs -> ConfigureServices method, add this line of code:
services.AddSingleton<ILoggerFactory>(new LoggerFactory());
In Startup.cs -> Configure method, change it looks like below:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
//add the following code here
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
loggerFactory.AddAzureWebAppDiagnostics(
new AzureAppServicesDiagnosticsSettings
{
OutputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss zzz} [{Level}] {RequestId}-{SourceContext}: {Message}{NewLine}{Exception}"
}
);
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
then add following code in the HomeController.cs:
private readonly ILogger _logger;
public HomeController(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<HomeController>();
}
public IActionResult Index()
{
_logger.LogInformation("This is a Log information!!!");
_logger.LogError("This is a Logger error!!!");
return View();
}
2.After publish to azure, in azure portal -> the web app -> App service logs, set the blob storage. The screenshot as below:
3.Run the web, and check the logs in the specified blob storage:
I make all changes on my api to use Azure Ad with this and this link features, but when the api is deployed, I need to make the user who gets the Url https://myapi.com/swagger (for example) to redirect it to azure Ad login,then know if the client have rights or not to use this api and redirect it again to my api and show the enpoints he have access.
I make some changes on startup.cs to use OpenIdConnect
//Add AddAzureAdBearer Auth options
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = OpenIdConnectDefaults.AuthenticationScheme;
//options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddOpenIdConnect(option =>
{
option.ClientId = Client;
option.Authority = $"{Instance}/{Tenant}";
option.SignedOutRedirectUri = "https://localhost:44308";
option.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
option.SaveTokens = true;
option.Events = new OpenIdConnectEvents
{
OnRemoteFailure = context =>
{
context.HandleResponse();
return Task.CompletedTask;
}
};
})
.AddCookie()
.AddAzureAdBearer(options => _configuration.Bind("Ad", options));
And I add a HomeController to redirect to swagger UI:
[Authorize]
public class HomeController : Controller
{
[HttpGet("")]
public ActionResult Index()
{
return Redirect("~/swagger");
}
}
When I launch the api, it works as spected, but when y write https://{host:port}/swagger it does not work, don't hit the authentication process and goes to https://{host:port}/swagger/index.html automatically.
How can I fix this?
I'm working with net core 2.0 and Swashbuckle for swagger.
You you need to add Swagger support to ConfigureServices(IServiceCollection services) and to Configure(IApplicationBuilder app, IHostingEnvironment env) in your application’s Startup.cs file. To do so, you need to create a SwaggerServiceExtensions class and add the necessary code to support Swagger in your app.
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Swashbuckle.AspNetCore.Swagger;
namespace JwtSwaggerDemo.Infrastructure
{
public static class SwaggerServiceExtensions
{
public static IServiceCollection AddSwaggerDocumentation(this IServiceCollection services)
{
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1.0", new Info { Title = "Main API v1.0", Version = "v1.0" });
c.AddSecurityDefinition("Bearer", new ApiKeyScheme
{
Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
Name = "Authorization",
In = "header",
Type = "apiKey"
});
});
return services;
}
public static IApplicationBuilder UseSwaggerDocumentation(this IApplicationBuilder app)
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1.0/swagger.json", "Versioned API v1.0");
c.DocExpansion("none");
});
return app;
}
}
}
Changes in Startup.cs file
Using the above class, the only thing you need to do in your Startup.cs file is the following:
namespace JwtSwaggerDemo
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
public IServiceProvider ConfigureServices(IServiceCollection services)
{
//... rest of services configuration
services.AddSwaggerDocumentation();
//...
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
//.... rest of app configuration
app.UseSwaggerDocumentation();
}
//.... rest of app configuration
}
}
}
Authorize requests in Swagger UI
Now, when you load the Swagger’s UI address (e.g: https://localhost:44321/swagger/#/), you will see an Authorize button at the top. Clicking on it leads to a modal window, which allows you to authorize your app with a JWT token, by adding Bearer in the value input field.
SignalR works on localhost but doesn't work when is deployed in Azure
Asp.net Core 1.0.0 (.Net Framework 4.6.1)
SignalR.Core 2.2.1
public static void UseSignalR2(this IApplicationBuilder app)
{
app.UseAppBuilder(appBuilder => {
appBuilder.MapSignalR(new HubConfiguration());
});
GlobalHost.HubPipeline.AddModule(new ErrorHandlingPipelineModule());
GlobalHost.HubPipeline.AddModule(new LoggingPipelineModule());
}
SignalR.js 2.2.1 with default settings
$.connection.hub.url = '/signalr';
Expected behavior
200 for url:
https://(name).azurewebsites.com/signalr/negotiate?clientProtocol=1.5&connectionData=%5B%7B%22name%22%3A%22productsimporthub%22%7D%5D&_=1472811629592
Actual behavior
/signalr/negotiate - on localhost returns 200 but for deployed app in azure returns 404
/signalr - works on both - Protocol error: Unknown transport.
/signalr/hubs - works on both - returns the SignalR js correctly
To find out the real cause of the issue you need to navigate to the negotiate url, and look for the response.
If the response tells you something about a 'CryptographicException: The data protection operation was unsuccessful...'. This is how to fix it.
1) Create a custom IDataProtectionProvider
2) Configure signalr
internal class MachineKeyProtectionProvider : IDataProtectionProvider
{
public IDataProtector Create(params string[] purposes)
{
return new MachineKeyDataProtector(purposes);
}
}
internal class MachineKeyDataProtector : IDataProtector
{
private readonly string[] _purposes;
public MachineKeyDataProtector(string[] purposes)
{
_purposes = purposes;
}
public byte[] Protect(byte[] userData)
{
//return MachineKey.Protect(userData, _purposes);
return userData;
}
public byte[] Unprotect(byte[] protectedData)
{
//return System.Web.Security.MachineKey.Unprotect(protectedData, _purposes);
return protectedData;
}
}
I use katana extension methods to bridge the IAppBuilder to IApplicationBuilder.
This allows your owin middleware to connect to asp.net core. It is important to use the RunSignalr method
app.UseAppBuilder(appBuilder =>
{
appBuilder.SetDataProtectionProvider(new MachineKeyProtectionProvider());
appBuilder.Map("/signalr", map =>
{
var hubConfiguration = new HubConfiguration
{
EnableDetailedErrors = true
};
map.RunSignalR(hubConfiguration);
});
});