using Microsoft.Extensions.Options;
using System.Net;
namespace LittleShop.Middleware;
///
/// Middleware to restrict access to admin endpoints based on IP address
/// Optional defense-in-depth measure (reverse proxy is preferred approach)
///
public class IPWhitelistMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
private readonly IPWhitelistOptions _options;
public IPWhitelistMiddleware(
RequestDelegate next,
ILogger logger,
IOptions options)
{
_next = next;
_logger = logger;
_options = options.Value;
}
public async Task InvokeAsync(HttpContext context)
{
// Skip if IP whitelist is disabled
if (!_options.Enabled)
{
await _next(context);
return;
}
var remoteIp = context.Connection.RemoteIpAddress;
// Check for X-Forwarded-For header if behind proxy
if (_options.UseForwardedHeaders && context.Request.Headers.ContainsKey("X-Forwarded-For"))
{
var forwardedFor = context.Request.Headers["X-Forwarded-For"].ToString();
var ips = forwardedFor.Split(',', StringSplitOptions.RemoveEmptyEntries);
if (ips.Length > 0 && IPAddress.TryParse(ips[0].Trim(), out var parsedIp))
{
remoteIp = parsedIp;
}
}
// Check for X-Real-IP header if behind nginx
if (_options.UseForwardedHeaders && context.Request.Headers.ContainsKey("X-Real-IP"))
{
var realIp = context.Request.Headers["X-Real-IP"].ToString();
if (IPAddress.TryParse(realIp, out var parsedIp))
{
remoteIp = parsedIp;
}
}
if (remoteIp == null)
{
_logger.LogWarning("Unable to determine client IP address");
context.Response.StatusCode = 403;
await context.Response.WriteAsJsonAsync(new
{
error = "Access denied",
message = "Unable to determine client IP address"
});
return;
}
// Check if IP is whitelisted
if (!IsIPWhitelisted(remoteIp))
{
_logger.LogWarning("Access denied for IP {IP} to {Path}", remoteIp, context.Request.Path);
context.Response.StatusCode = 403;
await context.Response.WriteAsJsonAsync(new
{
error = "Access denied",
message = "Admin access is restricted to authorized networks",
clientIp = _options.ShowClientIP ? remoteIp.ToString() : "hidden"
});
return;
}
_logger.LogDebug("IP {IP} whitelisted for {Path}", remoteIp, context.Request.Path);
await _next(context);
}
private bool IsIPWhitelisted(IPAddress clientIp)
{
// Localhost is always allowed
if (IPAddress.IsLoopback(clientIp))
{
return true;
}
// Check individual IPs
foreach (var allowedIp in _options.WhitelistedIPs)
{
if (IPAddress.TryParse(allowedIp, out var parsed) && clientIp.Equals(parsed))
{
return true;
}
}
// Check CIDR ranges
foreach (var cidr in _options.WhitelistedCIDRs)
{
if (IsInCIDRRange(clientIp, cidr))
{
return true;
}
}
return false;
}
private bool IsInCIDRRange(IPAddress clientIp, string cidr)
{
try
{
var parts = cidr.Split('/');
if (parts.Length != 2)
{
_logger.LogWarning("Invalid CIDR format: {CIDR}", cidr);
return false;
}
var networkAddress = IPAddress.Parse(parts[0]);
var prefixLength = int.Parse(parts[1]);
// Convert to byte arrays
var clientBytes = clientIp.GetAddressBytes();
var networkBytes = networkAddress.GetAddressBytes();
// Check if same address family
if (clientBytes.Length != networkBytes.Length)
{
return false;
}
// Calculate network mask
var maskBytes = new byte[networkBytes.Length];
var fullBytes = prefixLength / 8;
var remainingBits = prefixLength % 8;
// Set full bytes to 255
for (int i = 0; i < fullBytes; i++)
{
maskBytes[i] = 255;
}
// Set remaining bits
if (remainingBits > 0 && fullBytes < maskBytes.Length)
{
maskBytes[fullBytes] = (byte)(255 << (8 - remainingBits));
}
// Compare masked addresses
for (int i = 0; i < clientBytes.Length; i++)
{
if ((clientBytes[i] & maskBytes[i]) != (networkBytes[i] & maskBytes[i]))
{
return false;
}
}
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error checking CIDR range {CIDR}", cidr);
return false;
}
}
}
///
/// Configuration options for IP whitelist middleware
///
public class IPWhitelistOptions
{
public const string SectionName = "IPWhitelist";
///
/// Enable IP whitelist enforcement
///
public bool Enabled { get; set; } = false;
///
/// Use X-Forwarded-For and X-Real-IP headers (when behind reverse proxy)
///
public bool UseForwardedHeaders { get; set; } = true;
///
/// Show client IP in error response (for debugging)
///
public bool ShowClientIP { get; set; } = false;
///
/// List of whitelisted IP addresses
///
public List WhitelistedIPs { get; set; } = new()
{
"127.0.0.1",
"::1"
};
///
/// List of whitelisted CIDR ranges
///
public List WhitelistedCIDRs { get; set; } = new()
{
"192.168.0.0/16", // Private network Class C
"10.0.0.0/8", // Private network Class A
"172.16.0.0/12" // Private network Class B
};
}