Move APIKeys to file and allow read vs read/write

This commit is contained in:
2023-11-23 11:41:58 -05:00
parent 15f8385005
commit a7045ea9a4
12 changed files with 100 additions and 17 deletions

View File

@@ -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<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
{
return await _context.TodoItems.ToListAsync();
@@ -21,6 +21,7 @@ namespace TodoApi.Controllers
// GET: api/TodoItems/5
[HttpGet("{id}")]
[ApiKeyCanRead]
public async Task<ActionResult<TodoItem>> 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<IActionResult> 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<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
{
_context.TodoItems.Add(todoItem);
@@ -77,6 +80,7 @@ namespace TodoApi.Controllers
// DELETE: api/TodoItems/5
[HttpDelete("{id}")]
[ApiKeyCanWrite]
public async Task<IActionResult> DeleteTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);

View File

@@ -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();

View File

@@ -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<IApiKeyValidator>();
// 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);
}
}

View File

@@ -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<string>? apiKeys) : IApiKeyValidator
public class ApiKeyValidator(ApiKeys? apiKeys) : IApiKeyValidator
{
private readonly List<string>? _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);
}
}
}

12
Helpers/ApiKeys.cs Normal file
View File

@@ -0,0 +1,12 @@
using Newtonsoft.Json;
namespace TodoApi.Helpers;
public class ApiKeys
{
[JsonProperty("read_only")]
public List<string> ReadOnly { get; set; } = [];
[JsonProperty("read_write")]
public List<string> ReadWrite { get; set; } = [];
}

View File

@@ -0,0 +1,12 @@
namespace TodoApi;
public static class ExtensionMethods
{
public static bool Contains(this IEnumerable<string> source, string toCheck, StringComparison comp)
{
return
source != null &&
!string.IsNullOrEmpty(toCheck) &&
source.Any(x => string.Compare(x, toCheck, comp) == 0);
}
}

View File

@@ -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<TodoContext>(options => options.UseSqlite(connectionString));
//setup APIKey validation using APIKeys from config file
var apiKeys = builder.Configuration.GetSection("Authentication").GetValue<List<string>>("APIKeys");
//setup APIKey validation using apikey.json file
ApiKeys apiKeys = new();
try
{
string? apiKeyFilename = builder.Configuration.GetValue<string>("APIKeyFile");
if (File.Exists(apiKeyFilename))
{
string jsonText = File.ReadAllText(apiKeyFilename);
ApiKeys? apiKeysTemp = JsonConvert.DeserializeObject<ApiKeys>(jsonText);
if (apiKeysTemp != null) apiKeys = apiKeysTemp;
}
}
catch {}
builder.Services.AddSingleton<IApiKeyValidator, ApiKeyValidator>(_ => new ApiKeyValidator(apiKeys));
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle

View File

@@ -20,11 +20,12 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>
<ItemGroup>
<Content Include="data\*.*">
<Content Include="data\todo.db*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>

View File

@@ -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",

8
data/apikeys.json Normal file
View File

@@ -0,0 +1,8 @@
{
"read_only": [
"0a7eefbb-17f5-4299-882b-94719461a896"
],
"read_write": [
"8aa58ea6-0743-4598-b5ee-24a6842852ba"
]
}

BIN
data/todo.db-shm Normal file

Binary file not shown.

BIN
data/todo.db-wal Normal file

Binary file not shown.