From 15f8385005b4ea72924eecd64a7c83846670c383 Mon Sep 17 00:00:00 2001 From: Jeff Jumper Date: Wed, 22 Nov 2023 20:53:06 -0500 Subject: [PATCH] Add APIKey authentication --- .gitignore | 14 +++++----- .vscode/launch.json | 37 +++++++++++++++++++++++++ .vscode/tasks.json | 41 +++++++++++++++++++++++++++ Controllers/TodoItemsController.cs | 16 +++-------- Helpers/ApiKeyAttribute.cs | 27 ++++++++++++++++++ Helpers/ApiKeyValidator.cs | 20 ++++++++++++++ Program.cs | 43 +++++++++++++++++++++++++---- appsettings.json | 7 ++++- data/todo.db-shm | Bin 32768 -> 0 bytes data/todo.db-wal | 0 10 files changed, 179 insertions(+), 26 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 Helpers/ApiKeyAttribute.cs create mode 100644 Helpers/ApiKeyValidator.cs delete mode 100644 data/todo.db-shm delete mode 100644 data/todo.db-wal diff --git a/.gitignore b/.gitignore index 9d97689..2e018f0 100644 --- a/.gitignore +++ b/.gitignore @@ -399,12 +399,12 @@ FodyWeavers.xsd *.sln.iml # ---> VisualStudioCode -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -!.vscode/*.code-snippets +# .vscode/* +# !.vscode/settings.json +# !.vscode/tasks.json +# !.vscode/launch.json +# !.vscode/extensions.json +# !.vscode/*.code-snippets # Local History for Visual Studio Code .history/ @@ -450,7 +450,7 @@ project.lock.json nupkg/ # Visual Studio Code -.vscode +# .vscode # Rider .idea diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..65bf423 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,37 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md. + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "launchSettingsProfile": "http", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/bin/Debug/net8.0/TodoApi.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)", + "uriFormat": "%s/swagger/index.html" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..d83da10 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/TodoApi.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/TodoApi.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/TodoApi.sln" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Controllers/TodoItemsController.cs b/Controllers/TodoItemsController.cs index 3f2310c..605ffc6 100644 --- a/Controllers/TodoItemsController.cs +++ b/Controllers/TodoItemsController.cs @@ -1,24 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using TodoApi.Helpers; using TodoApi.Models; namespace TodoApi.Controllers { [Route("api/[controller]")] [ApiController] - public class TodoItemsController : ControllerBase + [ApiKey] + public class TodoItemsController(TodoContext context) : ControllerBase { - private readonly TodoContext _context; - - public TodoItemsController(TodoContext context) - { - _context = context; - } + private readonly TodoContext _context = context; // GET: api/TodoItems [HttpGet] diff --git a/Helpers/ApiKeyAttribute.cs b/Helpers/ApiKeyAttribute.cs new file mode 100644 index 0000000..7052281 --- /dev/null +++ b/Helpers/ApiKeyAttribute.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace TodoApi.Helpers; + +public class ApiKeyAttribute : 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.Validate(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 new file mode 100644 index 0000000..297a461 --- /dev/null +++ b/Helpers/ApiKeyValidator.cs @@ -0,0 +1,20 @@ +namespace TodoApi.Helpers +{ + public interface IApiKeyValidator + { + bool Validate(string? apiKey); + } + + public class ApiKeyValidator(List? apiKeys) : IApiKeyValidator + { + private readonly List? _apiKeys = apiKeys; + + public bool Validate(string? apiKey) + { + if (_apiKeys == null) return false; + + // Verify the provided apiKey is in our configuration + return _apiKeys.Contains(apiKey!.ToLower()); + } + } +} diff --git a/Program.cs b/Program.cs index 767b2a7..e5a8d9e 100644 --- a/Program.cs +++ b/Program.cs @@ -1,33 +1,64 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Data.Sqlite; +using TodoApi.Helpers; using TodoApi.Models; +using Microsoft.OpenApi.Models; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); +//Read the SQLite connection string from config file and add a DBContext var connectionString = new SqliteConnectionStringBuilder(builder.Configuration.GetConnectionString("TodoDatabase")) { Mode = SqliteOpenMode.ReadWriteCreate }.ToString(); -builder.Services.AddDbContext(options => - options.UseSqlite(connectionString)); +builder.Services.AddDbContext(options => options.UseSqlite(connectionString)); + +//setup APIKey validation using APIKeys from config file +var apiKeys = builder.Configuration.GetSection("Authentication").GetValue>("APIKeys"); +builder.Services.AddSingleton(_ => new ApiKeyValidator(apiKeys)); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new OpenApiInfo { Title = "ServiceName", Version = "1" }); + c.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme + { + Name = "X-API-KEY", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey, + Description = "Authorization by X-API-KEY inside request's header", + Scheme = "ApiKeyScheme" + }); + + var key = new OpenApiSecurityScheme() + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "ApiKey" + }, + In = ParameterLocation.Header + }; + + var requirement = new OpenApiSecurityRequirement { { key, new List() } }; + + c.AddSecurityRequirement(requirement); +}); var app = builder.Build(); // Configure the HTTP request pipeline. // if (app.Environment.IsDevelopment()) // { - app.UseSwagger(); - app.UseSwaggerUI(); +app.UseSwagger(); +app.UseSwaggerUI(); // } -app.UseHttpsRedirection(); +app.UseAuthentication(); app.UseAuthorization(); diff --git a/appsettings.json b/appsettings.json index 5ae76e4..23cd1f0 100644 --- a/appsettings.json +++ b/appsettings.json @@ -2,6 +2,11 @@ "ConnectionStrings": { "TodoDatabase": "Data Source=data/todo.db" }, + "Authentication": { + "APIKeys": [ + "0a7eefbb-17f5-4299-882b-94719461a896" + ] + }, "Logging": { "LogLevel": { "Default": "Information", @@ -9,4 +14,4 @@ } }, "AllowedHosts": "*" -} +} \ No newline at end of file diff --git a/data/todo.db-shm b/data/todo.db-shm deleted file mode 100644 index fe9ac2845eca6fe6da8a63cd096d9cf9e24ece10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeIuAr62r3