Skip to content

Commit 41d7a83

Browse files
committed
Add IP Address Whitelisting and enhance validation logic
1 parent fbd1754 commit 41d7a83

16 files changed

+974
-72
lines changed

README.md

Lines changed: 185 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ A flexible and lightweight API key authentication library for ASP.NET Core appli
1515
- [Installation](#installation)
1616
- [How to Pass API Keys](#how-to-pass-api-keys)
1717
- [Configuration](#configuration)
18+
- [IP Address Whitelisting](#ip-address-whitelisting)
1819
- [Usage Patterns](#usage-patterns)
1920
- [Advanced Customization](#advanced-customization)
2021
- [OpenAPI/Swagger Integration](#openapiswagger-integration)
@@ -30,18 +31,14 @@ AspNetCore.SecurityKey provides a complete API key authentication solution for A
3031

3132
**Key Features:**
3233

33-
- 🔑 **Multiple Input Sources** - API keys via headers, query parameters, or cookies
34-
- 🛡️ **Flexible Authentication** - Works with ASP.NET Core's built-in authentication or as standalone middleware
35-
- 🔧 **Extensible Design** - Custom validation and extraction logic support
36-
- 📝 **Rich Integration** - Controller attributes, middleware, and minimal API support
37-
- 📖 **OpenAPI Support** - Automatic Swagger/OpenAPI documentation generation (.NET 9+)
38-
-**High Performance** - Minimal overhead with optional caching
39-
- 🏗️ **Multiple Deployment Patterns** - Attribute-based, middleware, or endpoint filters
40-
41-
**Supported Frameworks:**
42-
43-
- .NET 8.0+
44-
- ASP.NET Core 8.0+
34+
- **Multiple Input Sources** - API keys via headers, query parameters, or cookies
35+
- **Flexible Authentication** - Works with ASP.NET Core's built-in authentication or as standalone middleware
36+
- **IP Address Whitelisting** - Restrict API access by IP addresses and network ranges (IPv4 and IPv6)
37+
- **Extensible Design** - Custom validation and extraction logic support
38+
- **Rich Integration** - Controller attributes, middleware, and minimal API support
39+
- **OpenAPI Support** - Automatic Swagger/OpenAPI documentation generation (.NET 9+)
40+
- **High Performance** - Minimal overhead with optional caching and timing-attack protection
41+
- **Multiple Deployment Patterns** - Attribute-based, middleware, or endpoint filters
4542

4643
## Quick Start
4744

@@ -151,6 +148,34 @@ Support multiple valid API keys using semicolon separation:
151148
}
152149
```
153150

151+
### Enhanced Configuration Format
152+
153+
For advanced scenarios with IP whitelisting and multiple keys, use the enhanced configuration format:
154+
155+
```json
156+
{
157+
"SecurityKey": {
158+
"AllowedKeys": [
159+
"01HSGVBGWXWDWTFGTJSYFXXDXQ",
160+
"01HSGVBSF99SK6XMJQJYF0X3WQ",
161+
"01HSGVAH2M5WVQYG4YPT7FNK4K8"
162+
],
163+
"AllowedAddresses": [
164+
"192.168.1.100",
165+
"10.0.0.1",
166+
"203.0.113.50",
167+
"::1"
168+
],
169+
"AllowedNetworks": [
170+
"192.168.0.0/16",
171+
"10.0.0.0/8",
172+
"172.16.0.0/12",
173+
"2001:db8::/32"
174+
]
175+
}
176+
}
177+
```
178+
154179
### Advanced Options
155180

156181
Customize key extraction and validation behavior:
@@ -185,6 +210,107 @@ builder.Services.AddSecurityKey(options =>
185210
});
186211
```
187212

213+
## IP Address Whitelisting
214+
215+
AspNetCore.SecurityKey provides built-in IP address whitelisting capabilities to restrict API access based on client IP addresses. This feature supports both IPv4 and IPv6 addresses, individual IPs, and network ranges using CIDR notation.
216+
217+
### Configuration
218+
219+
IP whitelisting is configured using the enhanced configuration format in `appsettings.json`:
220+
221+
```json
222+
{
223+
"SecurityKey": {
224+
"AllowedKeys": ["your-api-key-here"],
225+
"AllowedAddresses": [
226+
"192.168.1.100", // Specific IPv4 address
227+
"10.0.0.1", // Another IPv4 address
228+
"::1", // IPv6 localhost
229+
"2001:db8::1" // Specific IPv6 address
230+
],
231+
"AllowedNetworks": [
232+
"192.168.0.0/16", // Private network range
233+
"10.0.0.0/8", // Class A private network
234+
"172.16.0.0/12", // Class B private network
235+
"2001:db8::/32" // IPv6 network range
236+
]
237+
}
238+
}
239+
```
240+
241+
### How It Works
242+
243+
1. **No Restrictions**: If neither `AllowedAddresses` nor `AllowedNetworks` are configured, all IP addresses are allowed
244+
2. **Address Matching**: Client IP is checked against the `AllowedAddresses` list for exact matches
245+
3. **Network Matching**: Client IP is checked against the `AllowedNetworks` list using CIDR notation
246+
4. **Combined Logic**: A request is allowed if the IP matches either an allowed address OR falls within an allowed network
247+
248+
### Common Use Cases
249+
250+
#### Development Environment
251+
Allow only local development machines:
252+
253+
```json
254+
{
255+
"SecurityKey": {
256+
"AllowedKeys": ["dev-key-123"],
257+
"AllowedAddresses": [
258+
"127.0.0.1", // IPv4 localhost
259+
"::1" // IPv6 localhost
260+
],
261+
"AllowedNetworks": [
262+
"192.168.0.0/16" // Local network
263+
]
264+
}
265+
}
266+
```
267+
268+
#### Corporate Environment
269+
Allow only internal corporate networks:
270+
271+
```json
272+
{
273+
"SecurityKey": {
274+
"AllowedKeys": ["corporate-api-key"],
275+
"AllowedNetworks": [
276+
"10.0.0.0/8", // Corporate internal network
277+
"172.16.0.0/12", // Secondary corporate network
278+
"203.0.113.0/24" // Public-facing servers
279+
]
280+
}
281+
}
282+
```
283+
284+
285+
### Reverse Proxy Considerations
286+
287+
When running behind a reverse proxy (like nginx, IIS, or cloud load balancers), ensure proper configuration to get the real client IP:
288+
289+
```csharp
290+
// Configure forwarded headers
291+
builder.Services.Configure<ForwardedHeadersOptions>(options =>
292+
{
293+
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
294+
options.KnownProxies.Clear();
295+
options.KnownNetworks.Clear();
296+
});
297+
298+
var app = builder.Build();
299+
300+
// Use forwarded headers before SecurityKey middleware
301+
app.UseForwardedHeaders();
302+
app.UseSecurityKey();
303+
```
304+
305+
### Security Considerations
306+
307+
1. **Combine with HTTPS**: IP whitelisting should always be combined with HTTPS to prevent man-in-the-middle attacks
308+
2. **Network Ranges**: Be careful with broad network ranges like `0.0.0.0/0` or `::/0` as they allow all addresses
309+
3. **Dynamic IPs**: Consider that client IPs may change, especially for mobile clients or users behind NAT
310+
4. **Proxy Headers**: Validate that your reverse proxy configuration correctly forwards real client IPs
311+
5. **Logging**: Monitor failed authentication attempts to detect potential security issues
312+
313+
188314
## Usage Patterns
189315

190316
AspNetCore.SecurityKey supports multiple integration patterns to fit different application architectures and security requirements.
@@ -333,7 +459,7 @@ public class DatabaseSecurityKeyValidator : ISecurityKeyValidator
333459
_logger = logger;
334460
}
335461

336-
public async ValueTask<bool> Validate(string? value, CancellationToken cancellationToken = default)
462+
public async ValueTask<bool> Validate(string? value, IPAddress? ipAddress = null, CancellationToken cancellationToken = default)
337463
{
338464
if (string.IsNullOrEmpty(value))
339465
return false;
@@ -354,6 +480,13 @@ public class DatabaseSecurityKeyValidator : ISecurityKeyValidator
354480
return false;
355481
}
356482

483+
// Validate IP address if restrictions are configured
484+
if (!IsIpAddressAllowed(ipAddress, apiKey.AllowedIpAddresses, apiKey.AllowedNetworks))
485+
{
486+
_logger.LogWarning("API key {Key} used from unauthorized IP: {IpAddress}", value, ipAddress);
487+
return false;
488+
}
489+
357490
// Update last used timestamp
358491
await _repository.UpdateLastUsedAsync(value, DateTime.UtcNow, cancellationToken);
359492

@@ -366,7 +499,7 @@ public class DatabaseSecurityKeyValidator : ISecurityKeyValidator
366499
}
367500
}
368501

369-
public async ValueTask<ClaimsIdentity> Authenticate(string? value, CancellationToken cancellationToken = default)
502+
public async ValueTask<ClaimsIdentity> Authenticate(string? value, IPAddress? ipAddress = null, CancellationToken cancellationToken = default)
370503
{
371504
if (string.IsNullOrEmpty(value))
372505
return new ClaimsIdentity();
@@ -387,6 +520,12 @@ public class DatabaseSecurityKeyValidator : ISecurityKeyValidator
387520

388521
return identity;
389522
}
523+
524+
private bool IsIpAddressAllowed(IPAddress? ipAddress, string[]? allowedAddresses, string[]? allowedNetworks)
525+
{
526+
// Use the built-in whitelist functionality
527+
return SecurityKeyWhitelist.IsIpAllowed(ipAddress, allowedAddresses, allowedNetworks);
528+
}
390529
}
391530

392531
// Register custom validator
@@ -567,6 +706,21 @@ The `SecurityKeyDocumentTransformer` automatically configures the OpenAPI specif
567706
2. **Key Rotation**: Implement regular API key rotation policies
568707
3. **Logging**: Log authentication attempts without exposing the actual keys
569708
4. **Rate Limiting**: Implement rate limiting to prevent abuse
709+
5. **IP Whitelisting**: Use IP restrictions for additional security when possible
710+
6. **Timing Attack Protection**: The library uses cryptographic operations to prevent timing attacks
711+
712+
### Configuration Best Practices
713+
714+
1. **Environment Variables**: Store sensitive keys in environment variables or secure key vaults
715+
2. **Separate Keys**: Use different API keys for different environments (dev, staging, production)
716+
3. **Network Restrictions**: Configure IP whitelisting to restrict access to known sources
717+
4. **Monitor Usage**: Implement logging and monitoring to track API key usage patterns
718+
719+
### Performance Considerations
720+
721+
1. **Caching**: Enable caching for authentication results when using custom validators
722+
2. **Connection Pooling**: Use connection pooling for database-backed validators
723+
3. **Async Operations**: Leverage async/await patterns for I/O operations
570724

571725
## Troubleshooting
572726

@@ -609,6 +763,22 @@ builder.Services.AddScoped<IApiKeyRepository, ApiKeyRepository>();
609763
builder.Services.AddSecurityKey<CustomValidator>();
610764
```
611765

766+
**Issue**: IP whitelisting not working correctly
767+
768+
**Solution**: Check reverse proxy configuration and enable debug logging:
769+
770+
```csharp
771+
// Configure forwarded headers
772+
builder.Services.Configure<ForwardedHeadersOptions>(options =>
773+
{
774+
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
775+
});
776+
777+
var app = builder.Build();
778+
app.UseForwardedHeaders(); // Must be before UseSecurityKey()
779+
app.UseSecurityKey();
780+
```
781+
612782
### Debug Logging
613783

614784
Enable detailed logging to troubleshoot issues:
@@ -630,7 +800,7 @@ For complete working examples, see the samples in this repository:
630800

631801
- **[Sample.Controllers](samples/Sample.Controllers/)** - Controller-based API with attribute security
632802
- **[Sample.Middleware](samples/Sample.Middleware/)** - Middleware-based global security
633-
- **[Sample.MinimalApi](samples/Sample.MinimalApi/)** - Minimal APIs with endpoint filters
803+
- **[Sample.MinimalApi](samples/Sample.MinimalApi/)** - Minimal APIs with endpoint filters and IP whitelisting
634804

635805
Each sample includes:
636806

samples/Sample.MinimalApi/MinimalApi.http

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,6 @@ Accept: application/json
1818
X-API-KEY: 01HSGVBSF99SK6XMJQJYF0X3WQ
1919

2020

21-
###
22-
23-
GET {{HostAddress}}/config-debugger/
24-
Accept: application/json
25-
X-API-KEY: 01HSGVBSF99SK6XMJQJYF0X3WQ
26-
2721
###
2822

2923
GET {{HostAddress}}/current/

samples/Sample.MinimalApi/appsettings.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@
55
"Microsoft.AspNetCore": "Warning"
66
}
77
},
8-
"SecurityKey": "01HSGVBGWXWDWTFGTJSYFXXDXQ;01HSGVBSF99SK6XMJQJYF0X3WQ",
8+
"SecurityKey": {
9+
"AllowedKeys": [
10+
"01HSGVBGWXWDWTFGTJSYFXXDXQ",
11+
"01HSGVBSF99SK6XMJQJYF0X3WQ"
12+
],
13+
"AllowedAddresses": [
14+
"127.0.0.1", // IPv4 localhost
15+
"::1" // IPv6 localhost
16+
],
17+
"AllowedNetworks": [
18+
"192.168.0.0/16" // Local network
19+
]
20+
},
921
"AllowedHosts": "*"
1022
}

src/AspNetCore.SecurityKey/ISecurityKeyExtractor.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Net;
2+
13
using Microsoft.AspNetCore.Http;
24

35
namespace AspNetCore.SecurityKey;
@@ -17,4 +19,13 @@ public interface ISecurityKeyExtractor
1719
/// The extracted security API key if found; otherwise, <c>null</c>.
1820
/// </returns>
1921
string? GetKey(HttpContext? context);
22+
23+
/// <summary>
24+
/// Retrieves the remote IP address of the client from the specified HTTP context.
25+
/// </summary>
26+
/// <param name="context">The <see cref="HttpContext"/> instance containing the client's request information. Can be <see
27+
/// langword="null"/>.</param>
28+
/// <returns>The remote IP address of the client, or <see langword="null"/> if the context is <see
29+
/// langword="null"/> or the remote address is unavailable.</returns>
30+
IPAddress? GetRemoteAddress(HttpContext? context);
2031
}

src/AspNetCore.SecurityKey/ISecurityKeyValidator.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Net;
12
using System.Security.Claims;
23

34
namespace AspNetCore.SecurityKey;
@@ -12,20 +13,22 @@ public interface ISecurityKeyValidator
1213
/// Asynchronously validates the specified security API key.
1314
/// </summary>
1415
/// <param name="value">The security API key to validate. May be <c>null</c> if not provided in the request.</param>
16+
/// <param name="ipAddress">The IP address of the client making the request.</param>
1517
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param>
1618
/// <returns>
1719
/// A <see cref="ValueTask{Boolean}"/> that resolves to <c>true</c> if the security API key is valid; otherwise, <c>false</c>.
1820
/// </returns>
19-
ValueTask<bool> Validate(string? value, CancellationToken cancellationToken = default);
21+
ValueTask<bool> Validate(string? value, IPAddress? ipAddress = null, CancellationToken cancellationToken = default);
2022

2123
/// <summary>
2224
/// Asynchronously authenticates the specified security API key and produces a <see cref="ClaimsIdentity"/> if valid.
2325
/// </summary>
2426
/// <param name="value">The security API key to authenticate. May be <c>null</c> if not provided in the request.</param>
27+
/// <param name="ipAddress">The IP address of the client making the request.</param>
2528
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param>
2629
/// <returns>
2730
/// A <see cref="ValueTask{ClaimsIdentity}"/> representing the result of the authentication.
2831
/// The returned <see cref="ClaimsIdentity"/> should reflect the authenticated principal if the key is valid.
2932
/// </returns>
30-
ValueTask<ClaimsIdentity> Authenticate(string? value, CancellationToken cancellationToken = default);
33+
ValueTask<ClaimsIdentity> Authenticate(string? value, IPAddress? ipAddress = null, CancellationToken cancellationToken = default);
3134
}

src/AspNetCore.SecurityKey/SecurityKeyAuthenticationHandler.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
5555
if (string.IsNullOrEmpty(securityKey))
5656
return AuthenticateResult.NoResult();
5757

58-
var identity = await _securityKeyValidator.Authenticate(securityKey);
58+
var ipAddress = _securityKeyExtractor.GetRemoteAddress(Context);
59+
60+
var identity = await _securityKeyValidator.Authenticate(securityKey, ipAddress);
5961
if (!identity.IsAuthenticated)
6062
return AuthenticateResult.Fail("Invalid Security Key");
6163

src/AspNetCore.SecurityKey/SecurityKeyAuthorizationFilter.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@ public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
4949
ArgumentNullException.ThrowIfNull(context);
5050

5151
var securityKey = _securityKeyExtractor.GetKey(context.HttpContext);
52+
var ipAddress = _securityKeyExtractor.GetRemoteAddress(context.HttpContext);
5253

53-
if (await _securityKeyValidator.Validate(securityKey))
54+
if (await _securityKeyValidator.Validate(securityKey, ipAddress))
5455
return;
5556

5657
context.Result = new UnauthorizedResult();

src/AspNetCore.SecurityKey/SecurityKeyEndpointFilter.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@ public SecurityKeyEndpointFilter(
5151
ArgumentNullException.ThrowIfNull(context);
5252

5353
var securityKey = _securityKeyExtractor.GetKey(context.HttpContext);
54+
var ipAddress = _securityKeyExtractor.GetRemoteAddress(context.HttpContext);
5455

55-
if (await _securityKeyValidator.Validate(securityKey))
56+
if (await _securityKeyValidator.Validate(securityKey, ipAddress))
5657
return await next(context);
5758

5859
return Results.Unauthorized();

0 commit comments

Comments
 (0)