Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Joey Stallmeyer #256

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Crexi.Auctions.API/Controllers/AuctionsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Mvc;

namespace Crexi.Auctions.API.Controllers
{
[ApiController]
[Route("[controller]")]
public class AuctionsController : ControllerBase
{
private readonly ILogger<AuctionsController> _logger;

public AuctionsController(ILogger<AuctionsController> logger)
{
_logger = logger;
}

[HttpGet]
public int Get()
{
return 1;
}
}
}
22 changes: 22 additions & 0 deletions Crexi.Auctions.API/Crexi.Auctions.API.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\RateLimiter\Crexi.API.Common.RateLimiter.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Crexi.API.Common.RateLimiter.Interfaces;
using Crexi.API.Common.RateLimiter.Rules;

namespace Crexi.Auctions.API.Middleware.RateLimiter
{
public static class RateLimiterConfigurator
{
public static void ConfigureRateLimitingRules(IRateLimiter rateLimiter)
{
var usRateLimitRule = new FixedWindowRateLimitRule(
maxRequestsPerClient: 10,
timeWindow: TimeSpan.FromSeconds(10)
);

var euRateLimitRule = new TimeSinceLastCallRule(
requiredInterval: TimeSpan.FromSeconds(2)
);

var usConditionalRule = new ConditionalRateLimitRule(
condition: client => client.Location == "US",
rule: usRateLimitRule
);

var euConditionalRule = new ConditionalRateLimitRule(
condition: client => client.Location == "EU",
rule: euRateLimitRule
);

var compositeRule = new CompositeRateLimitRule(new[]
{
usConditionalRule,
euConditionalRule
});

rateLimiter.ConfigureResource("/Auctions", compositeRule);

var anotherRule = new FixedWindowRateLimitRule(50, TimeSpan.FromMinutes(10));
rateLimiter.ConfigureResource("/AuctionSettings", anotherRule);
}
}
}
42 changes: 42 additions & 0 deletions Crexi.Auctions.API/Middleware/RateLimiter/RateLimiterMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Crexi.API.Common.RateLimiter.Interfaces;

public class RateLimiterMiddleware
{
private readonly RequestDelegate _next;
private readonly IRateLimiter _rateLimiter;
private readonly ITokenToClientConverter _tokenToClientConverter;
private readonly ITokenExtractor _tokenExtractor;

public RateLimiterMiddleware(RequestDelegate next, IRateLimiter rateLimiter, ITokenToClientConverter tokenToClientConverter, ITokenExtractor tokenExtractor)
{
_next = next;
_rateLimiter = rateLimiter;
_tokenToClientConverter = tokenToClientConverter;
_tokenExtractor = tokenExtractor;
}

public async Task InvokeAsync(HttpContext context)
{
var accessToken = _tokenExtractor.ExtractToken(context);

var client = _tokenToClientConverter.ConvertTokenToClient(accessToken);

if (client == null)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsync("Invalid or malformed access token.");
return;
}

var resource = context.Request.Path.Value;

if (!_rateLimiter.IsRequestAllowed(client, resource))
{
context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
await context.Response.WriteAsync("Rate limit exceeded.");
return;
}

await _next(context);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

public interface ITokenExtractor
{
string ExtractToken(HttpContext context);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using Crexi.API.Common.RateLimiter.Models;

public interface ITokenToClientConverter
{
Client ConvertTokenToClient(string token);
}
13 changes: 13 additions & 0 deletions Crexi.Auctions.API/Middleware/RateLimiter/Util/TokenExtractor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
public class TokenExtractor : ITokenExtractor
{
public string ExtractToken(HttpContext context)
{
if (context.Request.Headers.TryGetValue("Authorization", out var authorizationHeader))
{
var token = authorizationHeader.ToString().Split(' ').Last();
return token;
}

return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Crexi.API.Common.RateLimiter.Models;
using System.IdentityModel.Tokens.Jwt;

public class TokenToClientConverter : ITokenToClientConverter
{
public Client ConvertTokenToClient(string token)
{
try
{
var handler = new JwtSecurityTokenHandler();
var jwtToken = handler.ReadJwtToken(token);

// Extract client ID and location from token claims
var clientId = jwtToken.Claims.FirstOrDefault(c => c.Type == "client_id")?.Value;
var clientLocation = jwtToken.Claims.FirstOrDefault(c => c.Type == "location")?.Value;

if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(clientLocation))
{
return null;
}

return new Client(clientId, clientLocation);
}
catch (Exception ex)
{
// TODO logging
return null;
}
}
}
67 changes: 67 additions & 0 deletions Crexi.Auctions.API/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using Crexi.API.Common.RateLimiter;
using Crexi.API.Common.RateLimiter.Interfaces;
using Crexi.Auctions.API.Middleware.RateLimiter;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder(args);

// Register services
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Crexi Auction API", Version = "v1" });

// Add the security definition for Bearer tokens
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme. Example: \"Bearer {token}\"",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});

// Add the security requirement to include the Bearer token globally
c.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});
});

builder.Services.AddSingleton<IRateLimiter, RateLimiter>();
builder.Services.AddTransient<ITokenToClientConverter, TokenToClientConverter>();
builder.Services.AddTransient<ITokenExtractor, TokenExtractor>();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization();

RateLimiterConfigurator.ConfigureRateLimitingRules(app.Services.GetRequiredService<IRateLimiter>());
app.UseMiddleware<RateLimiterMiddleware>();

app.MapControllers();

app.Run();
31 changes: 31 additions & 0 deletions Crexi.Auctions.API/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:42884",
"sslPort": 44366
}
},
"profiles": {
"Crexi.Auctions.API": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7145;http://localhost:5071",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
8 changes: 8 additions & 0 deletions Crexi.Auctions.API/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions Crexi.Auctions.API/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\RateLimiter\RateLimiter.csproj" />
<ProjectReference Include="..\RateLimiter\Crexi.API.Common.RateLimiter.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
Expand Down
13 changes: 0 additions & 13 deletions RateLimiter.Tests/RateLimiterTest.cs

This file was deleted.

Loading
Loading