Adding security

This commit is contained in:
Zeeshaun
2026-03-24 09:49:29 -05:00
parent 69648a3b8f
commit a3b74e62a9
59 changed files with 2480 additions and 0 deletions
@@ -0,0 +1,13 @@
using Microsoft.Extensions.DependencyInjection;
namespace LCU.Library.Internal.ApiSecurity;
public class ApiSecurityBuilder : IApiSecurityBuilder
{
public ApiSecurityBuilder(IServiceCollection services)
{
Services = services ?? throw new ArgumentNullException(nameof(services));
}
public IServiceCollection Services { get; }
}
@@ -0,0 +1,130 @@
using LCU.Library.Internal.ApiSecurity.Constants;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using LCU.Library.Internal.ApiSecurity.Enums;
using LCU.Library.Internal.ApiSecurity.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace LCU.Library.Internal.ApiSecurity.Attributes;
[AttributeUsage(validOn: AttributeTargets.Method)]
public class ApiKeyAttribute : Attribute, IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var apiSecurityConfiguration = context.HttpContext.RequestServices.GetRequiredService<ApiSecurityConfiguration>();
var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger(GetType().Name);
if (apiSecurityConfiguration.ExternalApiKeySet.Any())
{
if (!context.HttpContext.Request.Headers.TryGetValue(HeaderKeys.ApiKey, out var extractedApiKey))
{
switch (apiSecurityConfiguration.ApiKeyAuthenticationFailureBehavior)
{
case ApiKeyAuthenticationFailureBehavior.Warn:
logger.LogWarning(AuthMessages.NotProvidedMessage(HeaderKeys.ApiKey));
break;
case ApiKeyAuthenticationFailureBehavior.Error:
logger.LogError(AuthMessages.NotProvidedMessage(HeaderKeys.ApiKey));
context.Result = new ContentResult()
{
StatusCode = 401,
Content = AuthMessages.NotProvidedMessage(HeaderKeys.ApiKey)
};
return;
default:
throw new Exception("Invalid ApiKeyAuthenticationFailureBehavior Value");
}
}
if (!context.HttpContext.Request.Headers.TryGetValue(HeaderKeys.Client, out var extractedClient))
{
switch (apiSecurityConfiguration.ApiKeyAuthenticationFailureBehavior)
{
case ApiKeyAuthenticationFailureBehavior.Warn:
logger.LogWarning(AuthMessages.NotProvidedMessage(HeaderKeys.Client));
break;
case ApiKeyAuthenticationFailureBehavior.Error:
logger.LogError(AuthMessages.NotProvidedMessage(HeaderKeys.Client));
context.Result = new ContentResult()
{
StatusCode = 401,
Content = AuthMessages.NotProvidedMessage(HeaderKeys.Client)
};
return;
default:
throw new Exception("Invalid ApiKeyAuthenticationFailureBehavior Value");
}
}
if (!string.IsNullOrWhiteSpace(extractedClient))
{
if (apiSecurityConfiguration.ExternalApiKeySet.TryGetValue(extractedClient, out var apiKey))
{
if (!apiKey.Equals(extractedApiKey))
{
switch (apiSecurityConfiguration.ApiKeyAuthenticationFailureBehavior)
{
case ApiKeyAuthenticationFailureBehavior.Warn:
logger.LogWarning(AuthMessages.AuthFailureMessage(extractedClient));
break;
case ApiKeyAuthenticationFailureBehavior.Error:
logger.LogError(AuthMessages.AuthFailureMessage(extractedClient));
context.Result = new ContentResult()
{
StatusCode = 401,
Content = AuthMessages.AuthFailureMessage(extractedClient)
};
return;
default:
throw new Exception("Invalid ApiKeyAuthenticationFailureBehavior Value");
}
}
logger.LogInformation($"Valid auth received for Client: {extractedClient}");
}
else
{
switch (apiSecurityConfiguration.ApiKeyAuthenticationFailureBehavior)
{
case ApiKeyAuthenticationFailureBehavior.Warn:
logger.LogWarning(AuthMessages.ClientNotFoundMessage());
break;
case ApiKeyAuthenticationFailureBehavior.Error:
logger.LogError(AuthMessages.ClientNotFoundMessage());
context.Result = new ContentResult()
{
StatusCode = 401,
Content = AuthMessages.ClientNotFoundMessage()
};
return;
default:
throw new Exception("Invalid ApiKeyAuthenticationFailureBehavior Value");
}
}
}
}
else
{
switch (apiSecurityConfiguration.ApiKeyAuthenticationFailureBehavior)
{
case ApiKeyAuthenticationFailureBehavior.Warn:
logger.LogWarning(AuthMessages.EmptyKeySetMessage("ExternalApiKeySet"));
break;
case ApiKeyAuthenticationFailureBehavior.Error:
logger.LogError(AuthMessages.EmptyKeySetMessage("ExternalApiKeySet"));
context.Result = new ContentResult()
{
StatusCode = 401,
Content = AuthMessages.EmptyKeySetMessage("ExternalApiKeySet")
};
return;
default:
throw new Exception("Invalid ApiKeyAuthenticationFailureBehavior Value");
}
}
await next();
}
}
@@ -0,0 +1,29 @@
namespace LCU.Library.Internal.ApiSecurity.Constants;
public static class AuthMessages
{
private const string AuthFailureMessageTemplate = "Unauthorized client: ";
private const string ClientNotFoundMessageTemplate = "Client not found for extracted API Key";
private const string EmptyKeySetMessageTemplate = "Empty Key Set: ";
private const string NotProvidedMessageTemplate = "Header Not Provided: ";
public static string AuthFailureMessage(string input)
{
return $"{AuthFailureMessageTemplate}{input}";
}
public static string ClientNotFoundMessage()
{
return $"{ClientNotFoundMessageTemplate}";
}
public static string EmptyKeySetMessage(string input)
{
return $"{EmptyKeySetMessageTemplate}{input}";
}
public static string NotProvidedMessage(string input)
{
return $"{NotProvidedMessageTemplate}{input}";
}
}
@@ -0,0 +1,7 @@
namespace LCU.Library.Internal.ApiSecurity.Constants;
public static class HeaderKeys
{
public const string ApiKey = "ApiKey";
public const string Client = "Client";
}
@@ -0,0 +1,7 @@
namespace LCU.Library.Internal.ApiSecurity.Enums;
public enum ApiKeyAuthenticationFailureBehavior
{
Error,
Warn
}
@@ -0,0 +1,7 @@
namespace LCU.Library.Internal.ApiSecurity.Enums;
public enum ApiKeySetName
{
InternalKeySet,
ExternalKeySet
}
@@ -0,0 +1,11 @@
using Microsoft.Extensions.DependencyInjection;
namespace LCU.Library.Internal.ApiSecurity;
public interface IApiSecurityBuilder
{
/// <summary>
/// The application services.
/// </summary>
IServiceCollection Services { get; }
}
@@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Title>LCU.Library.Internal.ApiSecurity</Title>
<Authors>Landmark Credit Union</Authors>
<RepositoryType>Git</RepositoryType>
<Company>Landmark Credit Union</Company>
<AssemblyVersion>2.0.0</AssemblyVersion>
<FileVersion>2.0.0</FileVersion>
<PackageVersion>2.0.0</PackageVersion>
<PackageId>LCU.Library.Internal.ApiSecurity</PackageId>
<LangVersion>latest</LangVersion>
<TargetFramework>net8.0</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Version>2.0.0</Version>
<Description>
Standard LCU Api Security Library
2.0.0 - Updated to .NET 8
</Description>
<PackageReleaseNotes>
Do not throw on empty key sets, account for empty sets in auth
2.0.0 - Updated to .NET 8
</PackageReleaseNotes>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup>
</Project>
@@ -0,0 +1,13 @@
using LCU.Library.Internal.ApiSecurity.Models;
namespace LCU.Library.Internal.ApiSecurity;
public class LcuApiSecurity
{
private readonly ApiSecurityConfiguration _apiSecurityConfiguration;
public LcuApiSecurity(ApiSecurityConfiguration apiSecurityConfiguration)
{
_apiSecurityConfiguration = apiSecurityConfiguration;
}
}
@@ -0,0 +1,123 @@
using LCU.Library.Internal.ApiSecurity.Constants;
using Microsoft.AspNetCore.Http;
using LCU.Library.Internal.ApiSecurity.Enums;
using LCU.Library.Internal.ApiSecurity.Models;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Logging;
namespace LCU.Library.Internal.ApiSecurity.Middleware
{
public class ApiKeyMiddleware
{
private readonly ApiSecurityConfiguration _apiSecurityConfiguration;
private readonly ILogger _logger;
private readonly RequestDelegate _next;
public ApiKeyMiddleware(RequestDelegate next, ApiSecurityConfiguration apiSecurityConfiguration, ILoggerFactory loggerFactory)
{
_next = next;
_apiSecurityConfiguration = apiSecurityConfiguration;
_logger = loggerFactory.CreateLogger(GetType().Name);
}
public async Task InvokeAsync(HttpContext context)
{
var displayUrl = context.Request.GetDisplayUrl();
if (_apiSecurityConfiguration.ExternalApiKeySet.Any())
{
if (!context.Request.Headers.TryGetValue(HeaderKeys.ApiKey, out var extractedApiKey))
{
switch (_apiSecurityConfiguration.ApiKeyAuthenticationFailureBehavior)
{
case ApiKeyAuthenticationFailureBehavior.Warn:
_logger.LogWarning(AuthMessages.NotProvidedMessage(HeaderKeys.ApiKey));
break;
case ApiKeyAuthenticationFailureBehavior.Error:
_logger.LogError(AuthMessages.NotProvidedMessage(HeaderKeys.ApiKey));
context.Response.StatusCode = 401;
await context.Response.WriteAsync(AuthMessages.NotProvidedMessage(HeaderKeys.ApiKey));
return;
default:
throw new Exception("Invalid ApiKeyAuthenticationFailureBehavior Value");
}
}
if (!context.Request.Headers.TryGetValue(HeaderKeys.Client, out var extractedClient))
{
switch (_apiSecurityConfiguration.ApiKeyAuthenticationFailureBehavior)
{
case ApiKeyAuthenticationFailureBehavior.Warn:
_logger.LogWarning(AuthMessages.NotProvidedMessage(HeaderKeys.Client));
break;
case ApiKeyAuthenticationFailureBehavior.Error:
_logger.LogError(AuthMessages.NotProvidedMessage(HeaderKeys.Client));
context.Response.StatusCode = 401;
await context.Response.WriteAsync(AuthMessages.NotProvidedMessage(HeaderKeys.Client));
return;
default:
throw new Exception("Invalid ApiKeyAuthenticationFailureBehavior Value");
}
}
if (!string.IsNullOrWhiteSpace(extractedClient))
{
if (_apiSecurityConfiguration.ExternalApiKeySet.TryGetValue(extractedClient, out var apiKey))
{
if (!apiKey.Equals(extractedApiKey))
{
switch (_apiSecurityConfiguration.ApiKeyAuthenticationFailureBehavior)
{
case ApiKeyAuthenticationFailureBehavior.Warn:
_logger.LogWarning(AuthMessages.AuthFailureMessage(extractedClient));
break;
case ApiKeyAuthenticationFailureBehavior.Error:
_logger.LogError(AuthMessages.AuthFailureMessage(extractedClient));
context.Response.StatusCode = 401;
await context.Response.WriteAsync(AuthMessages.AuthFailureMessage(extractedClient));
return;
default:
throw new Exception("Invalid ApiKeyAuthenticationFailureBehavior Value");
}
}
_logger.LogInformation($"Valid auth received for Client: {extractedClient}");
}
else
{
switch (_apiSecurityConfiguration.ApiKeyAuthenticationFailureBehavior)
{
case ApiKeyAuthenticationFailureBehavior.Warn:
_logger.LogWarning(AuthMessages.ClientNotFoundMessage());
break;
case ApiKeyAuthenticationFailureBehavior.Error:
_logger.LogError(AuthMessages.ClientNotFoundMessage());
context.Response.StatusCode = 401;
await context.Response.WriteAsync(AuthMessages.ClientNotFoundMessage());
return;
default:
throw new Exception("Invalid ApiKeyAuthenticationFailureBehavior Value");
}
}
}
}
else
{
switch (_apiSecurityConfiguration.ApiKeyAuthenticationFailureBehavior)
{
case ApiKeyAuthenticationFailureBehavior.Warn:
_logger.LogWarning(AuthMessages.EmptyKeySetMessage("ExternalApiKeySet"));
break;
case ApiKeyAuthenticationFailureBehavior.Error:
_logger.LogError(AuthMessages.EmptyKeySetMessage("ExternalApiKeySet"));
context.Response.StatusCode = 401;
await context.Response.WriteAsync(AuthMessages.EmptyKeySetMessage("ExternalApiKeySet"));
return;
default:
throw new Exception("Invalid ApiKeyAuthenticationFailureBehavior Value");
}
}
await _next(context);
}
}
}
@@ -0,0 +1,39 @@
using System.ComponentModel.DataAnnotations;
using LCU.Library.Internal.ApiSecurity.Enums;
namespace LCU.Library.Internal.ApiSecurity.Models;
public class ApiSecurityConfiguration
{
public ApiKeyAuthenticationFailureBehavior ApiKeyAuthenticationFailureBehavior { get; set; }
[Required]
public Dictionary<string, string> ExternalApiKeySet { get; set; }
[Required]
public Dictionary<string, string> InternalApiKeySet { get; set; }
public ApiSecurityConfiguration(ApiKeyAuthenticationFailureBehavior apiKeyAuthenticationFailureBehavior = ApiKeyAuthenticationFailureBehavior.Error)
{
ApiKeyAuthenticationFailureBehavior = apiKeyAuthenticationFailureBehavior;
ExternalApiKeySet = new Dictionary<string, string>();
InternalApiKeySet = new Dictionary<string, string>();
}
public string ExtractApiKey(string clientKeyValue, ApiKeySetName keySetName)
{
string extractedApiKey = "";
if (keySetName == ApiKeySetName.InternalKeySet && !InternalApiKeySet.TryGetValue(clientKeyValue, out extractedApiKey))
{
throw new Exception("Your Internal Api Key is missing");
}
if (keySetName == ApiKeySetName.ExternalKeySet && !ExternalApiKeySet.TryGetValue(clientKeyValue, out extractedApiKey))
{
throw new Exception("Your External Api Key is missing");
}
return extractedApiKey;
}
}
@@ -0,0 +1,26 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using LCU.Library.Internal.ApiSecurity.Models;
namespace LCU.Library.Internal.ApiSecurity;
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds required API Security services.
/// </summary>
/// <param name="services">The service collection that API Security services are added to.</param>
/// <returns>A <see cref="IApiSecurityBuilder"/> that can be used to customize API Security functionality.</returns>
public static IApiSecurityBuilder AddApiKeySecurity(this IServiceCollection services, IConfiguration configuration)
{
services.AddLogging();
//TODO: Move this functionality from binding a config to making a call to an API Security API, only hold config in memory instead of file system
//This is less of a concern for containerized apps but still not ideal since the credentials will still be on the containers file system
var apiSecurityConfiguration = new ApiSecurityConfiguration();
configuration.GetSection("ApiSecurityConfiguration").Bind(apiSecurityConfiguration);
services.AddSingleton(apiSecurityConfiguration);
return new ApiSecurityBuilder(services);
}
}