From a7045ea9a4f0b7ad2263e48529a77d1412118f1b Mon Sep 17 00:00:00 2001 From: Jeff Jumper Date: Thu, 23 Nov 2023 11:41:58 -0500 Subject: [PATCH] Move APIKeys to file and allow read vs read/write --- Controllers/TodoItemsController.cs | 6 +++- ...Attribute.cs => ApiKeyCanReadAttribute.cs} | 4 +-- Helpers/ApiKeyCanWriteAttribute.cs | 27 ++++++++++++++++++ Helpers/ApiKeyValidator.cs | 22 ++++++++++---- Helpers/ApiKeys.cs | 12 ++++++++ Helpers/ExtensionMethods.cs | 12 ++++++++ Program.cs | 17 +++++++++-- TodoApi.csproj | 3 +- appsettings.json | 6 +--- data/apikeys.json | 8 ++++++ data/todo.db-shm | Bin 0 -> 32768 bytes data/todo.db-wal | Bin 0 -> 16512 bytes 12 files changed, 100 insertions(+), 17 deletions(-) rename Helpers/{ApiKeyAttribute.cs => ApiKeyCanReadAttribute.cs} (86%) create mode 100644 Helpers/ApiKeyCanWriteAttribute.cs create mode 100644 Helpers/ApiKeys.cs create mode 100644 Helpers/ExtensionMethods.cs create mode 100644 data/apikeys.json create mode 100644 data/todo.db-shm create mode 100644 data/todo.db-wal diff --git a/Controllers/TodoItemsController.cs b/Controllers/TodoItemsController.cs index 605ffc6..fe71ad4 100644 --- a/Controllers/TodoItemsController.cs +++ b/Controllers/TodoItemsController.cs @@ -7,13 +7,13 @@ namespace TodoApi.Controllers { [Route("api/[controller]")] [ApiController] - [ApiKey] public class TodoItemsController(TodoContext context) : ControllerBase { private readonly TodoContext _context = context; // GET: api/TodoItems [HttpGet] + [ApiKeyCanRead] public async Task>> GetTodoItems() { return await _context.TodoItems.ToListAsync(); @@ -21,6 +21,7 @@ namespace TodoApi.Controllers // GET: api/TodoItems/5 [HttpGet("{id}")] + [ApiKeyCanRead] public async Task> GetTodoItem(long id) { var todoItem = await _context.TodoItems.FindAsync(id); @@ -36,6 +37,7 @@ namespace TodoApi.Controllers // PUT: api/TodoItems/5 // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 [HttpPut("{id}")] + [ApiKeyCanWrite] public async Task PutTodoItem(long id, TodoItem todoItem) { if (id != todoItem.Id) @@ -67,6 +69,7 @@ namespace TodoApi.Controllers // POST: api/TodoItems // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 [HttpPost] + [ApiKeyCanWrite] public async Task> PostTodoItem(TodoItem todoItem) { _context.TodoItems.Add(todoItem); @@ -77,6 +80,7 @@ namespace TodoApi.Controllers // DELETE: api/TodoItems/5 [HttpDelete("{id}")] + [ApiKeyCanWrite] public async Task DeleteTodoItem(long id) { var todoItem = await _context.TodoItems.FindAsync(id); diff --git a/Helpers/ApiKeyAttribute.cs b/Helpers/ApiKeyCanReadAttribute.cs similarity index 86% rename from Helpers/ApiKeyAttribute.cs rename to Helpers/ApiKeyCanReadAttribute.cs index 7052281..945984d 100644 --- a/Helpers/ApiKeyAttribute.cs +++ b/Helpers/ApiKeyCanReadAttribute.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc.Filters; namespace TodoApi.Helpers; -public class ApiKeyAttribute : ActionFilterAttribute +public class ApiKeyCanReadAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext context) { @@ -14,7 +14,7 @@ public class ApiKeyAttribute : ActionFilterAttribute var apiKey = context.HttpContext.Request.Headers["X-API-KEY"]; // Validate the API key using the IApiKeyValidator service - if (string.IsNullOrEmpty(apiKey) || !apiKeyValidator.Validate(apiKey)) + if (string.IsNullOrEmpty(apiKey) || !apiKeyValidator.CanRead(apiKey)) { // If the API key is invalid, set the response status code to 401 Unauthorized context.Result = new UnauthorizedResult(); diff --git a/Helpers/ApiKeyCanWriteAttribute.cs b/Helpers/ApiKeyCanWriteAttribute.cs new file mode 100644 index 0000000..07768d2 --- /dev/null +++ b/Helpers/ApiKeyCanWriteAttribute.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace TodoApi.Helpers; + +public class ApiKeyCanWriteAttribute : ActionFilterAttribute +{ + public override void OnActionExecuting(ActionExecutingContext context) + { + // Get the required service to validate the API key + var apiKeyValidator = context.HttpContext.RequestServices.GetRequiredService(); + + // Get the API key from the X-API-KEY header + var apiKey = context.HttpContext.Request.Headers["X-API-KEY"]; + + // Validate the API key using the IApiKeyValidator service + if (string.IsNullOrEmpty(apiKey) || !apiKeyValidator.CanWrite(apiKey)) + { + // If the API key is invalid, set the response status code to 401 Unauthorized + context.Result = new UnauthorizedResult(); + return; + } + + // If the API key is valid, continue with the action execution + base.OnActionExecuting(context); + } +} diff --git a/Helpers/ApiKeyValidator.cs b/Helpers/ApiKeyValidator.cs index 297a461..8e19c4a 100644 --- a/Helpers/ApiKeyValidator.cs +++ b/Helpers/ApiKeyValidator.cs @@ -2,19 +2,29 @@ namespace TodoApi.Helpers { public interface IApiKeyValidator { - bool Validate(string? apiKey); + bool CanRead(string? apiKey); + bool CanWrite(string? apiKey); } - public class ApiKeyValidator(List? apiKeys) : IApiKeyValidator + public class ApiKeyValidator(ApiKeys? apiKeys) : IApiKeyValidator { - private readonly List? _apiKeys = apiKeys; + private readonly ApiKeys _apiKeys = apiKeys ?? new ApiKeys(); - public bool Validate(string? apiKey) + public bool CanRead(string? apiKey) { - if (_apiKeys == null) return false; + if (apiKey == null) return false; // Verify the provided apiKey is in our configuration - return _apiKeys.Contains(apiKey!.ToLower()); + return _apiKeys.ReadOnly.Contains(apiKey, StringComparison.OrdinalIgnoreCase) || + _apiKeys.ReadWrite.Contains(apiKey, StringComparison.OrdinalIgnoreCase); + } + + public bool CanWrite(string? apiKey) + { + if (apiKey == null) return false; + + // Verify the provided apiKey is in our configuration + return _apiKeys.ReadWrite.Contains(apiKey, StringComparison.OrdinalIgnoreCase); } } } diff --git a/Helpers/ApiKeys.cs b/Helpers/ApiKeys.cs new file mode 100644 index 0000000..05bc3ff --- /dev/null +++ b/Helpers/ApiKeys.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace TodoApi.Helpers; + +public class ApiKeys +{ + [JsonProperty("read_only")] + public List ReadOnly { get; set; } = []; + + [JsonProperty("read_write")] + public List ReadWrite { get; set; } = []; +} diff --git a/Helpers/ExtensionMethods.cs b/Helpers/ExtensionMethods.cs new file mode 100644 index 0000000..493f862 --- /dev/null +++ b/Helpers/ExtensionMethods.cs @@ -0,0 +1,12 @@ +namespace TodoApi; + +public static class ExtensionMethods +{ + public static bool Contains(this IEnumerable source, string toCheck, StringComparison comp) + { + return + source != null && + !string.IsNullOrEmpty(toCheck) && + source.Any(x => string.Compare(x, toCheck, comp) == 0); + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs index e5a8d9e..3075d21 100644 --- a/Program.cs +++ b/Program.cs @@ -3,6 +3,7 @@ using Microsoft.Data.Sqlite; using TodoApi.Helpers; using TodoApi.Models; using Microsoft.OpenApi.Models; +using Newtonsoft.Json; var builder = WebApplication.CreateBuilder(args); @@ -16,8 +17,20 @@ var connectionString = new SqliteConnectionStringBuilder(builder.Configuration.G }.ToString(); builder.Services.AddDbContext(options => options.UseSqlite(connectionString)); -//setup APIKey validation using APIKeys from config file -var apiKeys = builder.Configuration.GetSection("Authentication").GetValue>("APIKeys"); +//setup APIKey validation using apikey.json file +ApiKeys apiKeys = new(); +try +{ + string? apiKeyFilename = builder.Configuration.GetValue("APIKeyFile"); + if (File.Exists(apiKeyFilename)) + { + string jsonText = File.ReadAllText(apiKeyFilename); + + ApiKeys? apiKeysTemp = JsonConvert.DeserializeObject(jsonText); + if (apiKeysTemp != null) apiKeys = apiKeysTemp; + } +} +catch {} builder.Services.AddSingleton(_ => new ApiKeyValidator(apiKeys)); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle diff --git a/TodoApi.csproj b/TodoApi.csproj index db99554..8be019c 100644 --- a/TodoApi.csproj +++ b/TodoApi.csproj @@ -20,11 +20,12 @@ all + - + Always diff --git a/appsettings.json b/appsettings.json index 23cd1f0..154b879 100644 --- a/appsettings.json +++ b/appsettings.json @@ -2,11 +2,7 @@ "ConnectionStrings": { "TodoDatabase": "Data Source=data/todo.db" }, - "Authentication": { - "APIKeys": [ - "0a7eefbb-17f5-4299-882b-94719461a896" - ] - }, + "APIKeyFile": "data/apikeys.json", "Logging": { "LogLevel": { "Default": "Information", diff --git a/data/apikeys.json b/data/apikeys.json new file mode 100644 index 0000000..e2fac33 --- /dev/null +++ b/data/apikeys.json @@ -0,0 +1,8 @@ +{ + "read_only": [ + "0a7eefbb-17f5-4299-882b-94719461a896" + ], + "read_write": [ + "8aa58ea6-0743-4598-b5ee-24a6842852ba" + ] + } \ No newline at end of file diff --git a/data/todo.db-shm b/data/todo.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..da61a7457333d07f0fccb7cecd8e1a5031cde318 GIT binary patch literal 32768 zcmeI)y9ok85CG7=6yMPUU!?<;=Df{!=#?q>+#{Ty;Yg}<5f+Raee>9w;fVH zMfYx3+pd)W0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+KtBYEFzCl1rXY|lP&WU~ zWgl~Z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk L1PBly@CShptD7Z> literal 0 HcmV?d00001 diff --git a/data/todo.db-wal b/data/todo.db-wal new file mode 100644 index 0000000000000000000000000000000000000000..deeadd4b6a61458177acf6e2d0a30721b7b6041a GIT binary patch literal 16512 zcmeI&ze~eF7{>88-YHhBv0FPe(xF9hRdBXK$BKV|N<)zT2(;&rL4-na6*mXBE=6!C z2<}dfPL2g}bg?*f@*)U!?3nU>NiOhCc|W(kJiC`mUDhzT7eyEF@FE{1$R+n@1d_G-U~SHm)M6w009ILKmY**5I_I{1Q0*~ zfq4-~J1XxSg|)C^{ANelv8M6@r@FXVefccKdjaLX{^kYjhe7}W1Q0*~0R#|0009IL zKmdUx2xPRDVrt)J0qUL~1YRo)X2}bT^IN0I;l&$yfh1fu|A+tr2q1s}0tg_000Iag MfWV9b(|G~?19A^aa{vGU literal 0 HcmV?d00001