Move APIKeys to file and allow read vs read/write
This commit is contained in:
@@ -7,13 +7,13 @@ namespace TodoApi.Controllers
|
|||||||
{
|
{
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[ApiKey]
|
|
||||||
public class TodoItemsController(TodoContext context) : ControllerBase
|
public class TodoItemsController(TodoContext context) : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly TodoContext _context = context;
|
private readonly TodoContext _context = context;
|
||||||
|
|
||||||
// GET: api/TodoItems
|
// GET: api/TodoItems
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
|
[ApiKeyCanRead]
|
||||||
public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
|
public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
|
||||||
{
|
{
|
||||||
return await _context.TodoItems.ToListAsync();
|
return await _context.TodoItems.ToListAsync();
|
||||||
@@ -21,6 +21,7 @@ namespace TodoApi.Controllers
|
|||||||
|
|
||||||
// GET: api/TodoItems/5
|
// GET: api/TodoItems/5
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
|
[ApiKeyCanRead]
|
||||||
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
|
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
|
||||||
{
|
{
|
||||||
var todoItem = await _context.TodoItems.FindAsync(id);
|
var todoItem = await _context.TodoItems.FindAsync(id);
|
||||||
@@ -36,6 +37,7 @@ namespace TodoApi.Controllers
|
|||||||
// PUT: api/TodoItems/5
|
// PUT: api/TodoItems/5
|
||||||
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
|
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
|
||||||
[HttpPut("{id}")]
|
[HttpPut("{id}")]
|
||||||
|
[ApiKeyCanWrite]
|
||||||
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
|
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
|
||||||
{
|
{
|
||||||
if (id != todoItem.Id)
|
if (id != todoItem.Id)
|
||||||
@@ -67,6 +69,7 @@ namespace TodoApi.Controllers
|
|||||||
// POST: api/TodoItems
|
// POST: api/TodoItems
|
||||||
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
|
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
[ApiKeyCanWrite]
|
||||||
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
|
public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
|
||||||
{
|
{
|
||||||
_context.TodoItems.Add(todoItem);
|
_context.TodoItems.Add(todoItem);
|
||||||
@@ -77,6 +80,7 @@ namespace TodoApi.Controllers
|
|||||||
|
|
||||||
// DELETE: api/TodoItems/5
|
// DELETE: api/TodoItems/5
|
||||||
[HttpDelete("{id}")]
|
[HttpDelete("{id}")]
|
||||||
|
[ApiKeyCanWrite]
|
||||||
public async Task<IActionResult> DeleteTodoItem(long id)
|
public async Task<IActionResult> DeleteTodoItem(long id)
|
||||||
{
|
{
|
||||||
var todoItem = await _context.TodoItems.FindAsync(id);
|
var todoItem = await _context.TodoItems.FindAsync(id);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc.Filters;
|
|||||||
|
|
||||||
namespace TodoApi.Helpers;
|
namespace TodoApi.Helpers;
|
||||||
|
|
||||||
public class ApiKeyAttribute : ActionFilterAttribute
|
public class ApiKeyCanReadAttribute : ActionFilterAttribute
|
||||||
{
|
{
|
||||||
public override void OnActionExecuting(ActionExecutingContext context)
|
public override void OnActionExecuting(ActionExecutingContext context)
|
||||||
{
|
{
|
||||||
@@ -14,7 +14,7 @@ public class ApiKeyAttribute : ActionFilterAttribute
|
|||||||
var apiKey = context.HttpContext.Request.Headers["X-API-KEY"];
|
var apiKey = context.HttpContext.Request.Headers["X-API-KEY"];
|
||||||
|
|
||||||
// Validate the API key using the IApiKeyValidator service
|
// 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
|
// If the API key is invalid, set the response status code to 401 Unauthorized
|
||||||
context.Result = new UnauthorizedResult();
|
context.Result = new UnauthorizedResult();
|
||||||
27
Helpers/ApiKeyCanWriteAttribute.cs
Normal file
27
Helpers/ApiKeyCanWriteAttribute.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,19 +2,29 @@ namespace TodoApi.Helpers
|
|||||||
{
|
{
|
||||||
public interface IApiKeyValidator
|
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
|
// 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
12
Helpers/ApiKeys.cs
Normal 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; } = [];
|
||||||
|
}
|
||||||
12
Helpers/ExtensionMethods.cs
Normal file
12
Helpers/ExtensionMethods.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
Program.cs
17
Program.cs
@@ -3,6 +3,7 @@ using Microsoft.Data.Sqlite;
|
|||||||
using TodoApi.Helpers;
|
using TodoApi.Helpers;
|
||||||
using TodoApi.Models;
|
using TodoApi.Models;
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -16,8 +17,20 @@ var connectionString = new SqliteConnectionStringBuilder(builder.Configuration.G
|
|||||||
}.ToString();
|
}.ToString();
|
||||||
builder.Services.AddDbContext<TodoContext>(options => options.UseSqlite(connectionString));
|
builder.Services.AddDbContext<TodoContext>(options => options.UseSqlite(connectionString));
|
||||||
|
|
||||||
//setup APIKey validation using APIKeys from config file
|
//setup APIKey validation using apikey.json file
|
||||||
var apiKeys = builder.Configuration.GetSection("Authentication").GetValue<List<string>>("APIKeys");
|
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));
|
builder.Services.AddSingleton<IApiKeyValidator, ApiKeyValidator>(_ => new ApiKeyValidator(apiKeys));
|
||||||
|
|
||||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||||
|
|||||||
@@ -20,11 +20,12 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.0" />
|
<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" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="data\*.*">
|
<Content Include="data\todo.db*">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -2,11 +2,7 @@
|
|||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"TodoDatabase": "Data Source=data/todo.db"
|
"TodoDatabase": "Data Source=data/todo.db"
|
||||||
},
|
},
|
||||||
"Authentication": {
|
"APIKeyFile": "data/apikeys.json",
|
||||||
"APIKeys": [
|
|
||||||
"0a7eefbb-17f5-4299-882b-94719461a896"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
|
|||||||
8
data/apikeys.json
Normal file
8
data/apikeys.json
Normal 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
BIN
data/todo.db-shm
Normal file
Binary file not shown.
BIN
data/todo.db-wal
Normal file
BIN
data/todo.db-wal
Normal file
Binary file not shown.
Reference in New Issue
Block a user