From e4c7a1aeb917c64732e46ee5c2e3d319abe5eb87 Mon Sep 17 00:00:00 2001 From: Paul Welter Date: Thu, 21 Mar 2024 14:41:58 -0500 Subject: [PATCH] first progress --- .editorconfig | 52 +++++++++ .gitattributes | 63 +++++++++++ .github/dependabot.yml | 15 +++ .github/workflows/dotnet.yml | 99 ++++++++++++++++++ AspNetCore.Extensions.sln | 92 ++++++++++++++++ coverlet.runsettings | 14 +++ logo.png | Bin 0 -> 5007 bytes samples/Directory.Build.props | 38 +++++++ samples/Sample.Controllers/Controllers.http | 18 ++++ .../Controllers/AddressController.cs | 20 ++++ .../Controllers/UserController.cs | 20 ++++ .../Controllers/WeatherController.cs | 16 +++ samples/Sample.Controllers/Program.cs | 31 ++++++ .../Properties/launchSettings.json | 15 +++ .../Sample.Controllers.csproj | 19 ++++ samples/Sample.Controllers/appsettings.json | 12 +++ samples/Sample.Middleware/Middleware.http | 19 ++++ samples/Sample.Middleware/Program.cs | 46 ++++++++ .../Properties/launchSettings.json | 15 +++ .../Sample.Middleware.csproj | 20 ++++ samples/Sample.Middleware/appsettings.json | 10 ++ samples/Sample.MinimalApi/MinimalApi.http | 18 ++++ samples/Sample.MinimalApi/Program.cs | 50 +++++++++ .../Properties/launchSettings.json | 15 +++ .../Sample.MinimalApi.csproj | 20 ++++ samples/Sample.MinimalApi/appsettings.json | 10 ++ samples/Sample.Shared/Address.cs | 3 + samples/Sample.Shared/AddressFaker.cs | 20 ++++ samples/Sample.Shared/Sample.Shared.csproj | 9 ++ samples/Sample.Shared/User.cs | 3 + samples/Sample.Shared/UserFaker.cs | 18 ++++ samples/Sample.Shared/Weather.cs | 12 +++ samples/Sample.Shared/WeatherFaker.cs | 16 +++ .../AspNetCore.Extensions.Diagnostics.csproj | 12 +++ .../Class1.cs | 6 ++ .../ApplicationBuilderExtensions.cs | 11 ++ .../AspNetCore.Extensions.SecurityKey.csproj | 13 +++ .../AuthenticationBuilderExtensions.cs | 14 +++ .../DependencyInjectionExtensions.cs | 25 +++++ .../EndpointFilterExtensions.cs | 15 +++ .../ISecurityKeyExtractor.cs | 8 ++ .../ISecurityKeyValidator.cs | 6 ++ .../SecurityKeyAttribute.cs | 11 ++ .../SecurityKeyAuthenticationDefaults.cs | 6 ++ .../SecurityKeyAuthenticationHandler.cs | 51 +++++++++ .../SecurityKeyAuthenticationSchemeOptions.cs | 8 ++ .../SecurityKeyAuthorizationFilter.cs | 39 +++++++ .../SecurityKeyEndpointFilter.cs | 40 +++++++ .../SecurityKeyExtractor.cs | 34 ++++++ .../SecurityKeyLogger.cs | 13 +++ .../SecurityKeyMiddleware.cs | 47 +++++++++ .../SecurityKeyOptions.cs | 14 +++ .../SecurityKeyValidator.cs | 49 +++++++++ src/Directory.Build.props | 54 ++++++++++ ...etCore.Extensions.Diagnostics.Tests.csproj | 33 ++++++ .../UnitTest1.cs | 10 ++ ...etCore.Extensions.SecurityKey.Tests.csproj | 33 ++++++ .../UnitTest1.cs | 10 ++ test/Directory.Build.props | 46 ++++++++ 59 files changed, 1436 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/dotnet.yml create mode 100644 AspNetCore.Extensions.sln create mode 100644 coverlet.runsettings create mode 100644 logo.png create mode 100644 samples/Directory.Build.props create mode 100644 samples/Sample.Controllers/Controllers.http create mode 100644 samples/Sample.Controllers/Controllers/AddressController.cs create mode 100644 samples/Sample.Controllers/Controllers/UserController.cs create mode 100644 samples/Sample.Controllers/Controllers/WeatherController.cs create mode 100644 samples/Sample.Controllers/Program.cs create mode 100644 samples/Sample.Controllers/Properties/launchSettings.json create mode 100644 samples/Sample.Controllers/Sample.Controllers.csproj create mode 100644 samples/Sample.Controllers/appsettings.json create mode 100644 samples/Sample.Middleware/Middleware.http create mode 100644 samples/Sample.Middleware/Program.cs create mode 100644 samples/Sample.Middleware/Properties/launchSettings.json create mode 100644 samples/Sample.Middleware/Sample.Middleware.csproj create mode 100644 samples/Sample.Middleware/appsettings.json create mode 100644 samples/Sample.MinimalApi/MinimalApi.http create mode 100644 samples/Sample.MinimalApi/Program.cs create mode 100644 samples/Sample.MinimalApi/Properties/launchSettings.json create mode 100644 samples/Sample.MinimalApi/Sample.MinimalApi.csproj create mode 100644 samples/Sample.MinimalApi/appsettings.json create mode 100644 samples/Sample.Shared/Address.cs create mode 100644 samples/Sample.Shared/AddressFaker.cs create mode 100644 samples/Sample.Shared/Sample.Shared.csproj create mode 100644 samples/Sample.Shared/User.cs create mode 100644 samples/Sample.Shared/UserFaker.cs create mode 100644 samples/Sample.Shared/Weather.cs create mode 100644 samples/Sample.Shared/WeatherFaker.cs create mode 100644 src/AspNetCore.Extensions.Diagnostics/AspNetCore.Extensions.Diagnostics.csproj create mode 100644 src/AspNetCore.Extensions.Diagnostics/Class1.cs create mode 100644 src/AspNetCore.Extensions.SecurityKey/ApplicationBuilderExtensions.cs create mode 100644 src/AspNetCore.Extensions.SecurityKey/AspNetCore.Extensions.SecurityKey.csproj create mode 100644 src/AspNetCore.Extensions.SecurityKey/AuthenticationBuilderExtensions.cs create mode 100644 src/AspNetCore.Extensions.SecurityKey/DependencyInjectionExtensions.cs create mode 100644 src/AspNetCore.Extensions.SecurityKey/EndpointFilterExtensions.cs create mode 100644 src/AspNetCore.Extensions.SecurityKey/ISecurityKeyExtractor.cs create mode 100644 src/AspNetCore.Extensions.SecurityKey/ISecurityKeyValidator.cs create mode 100644 src/AspNetCore.Extensions.SecurityKey/SecurityKeyAttribute.cs create mode 100644 src/AspNetCore.Extensions.SecurityKey/SecurityKeyAuthenticationDefaults.cs create mode 100644 src/AspNetCore.Extensions.SecurityKey/SecurityKeyAuthenticationHandler.cs create mode 100644 src/AspNetCore.Extensions.SecurityKey/SecurityKeyAuthenticationSchemeOptions.cs create mode 100644 src/AspNetCore.Extensions.SecurityKey/SecurityKeyAuthorizationFilter.cs create mode 100644 src/AspNetCore.Extensions.SecurityKey/SecurityKeyEndpointFilter.cs create mode 100644 src/AspNetCore.Extensions.SecurityKey/SecurityKeyExtractor.cs create mode 100644 src/AspNetCore.Extensions.SecurityKey/SecurityKeyLogger.cs create mode 100644 src/AspNetCore.Extensions.SecurityKey/SecurityKeyMiddleware.cs create mode 100644 src/AspNetCore.Extensions.SecurityKey/SecurityKeyOptions.cs create mode 100644 src/AspNetCore.Extensions.SecurityKey/SecurityKeyValidator.cs create mode 100644 src/Directory.Build.props create mode 100644 test/AspNetCore.Extensions.Diagnostics.Tests/AspNetCore.Extensions.Diagnostics.Tests.csproj create mode 100644 test/AspNetCore.Extensions.Diagnostics.Tests/UnitTest1.cs create mode 100644 test/AspNetCore.Extensions.SecurityKey.Tests/AspNetCore.Extensions.SecurityKey.Tests.csproj create mode 100644 test/AspNetCore.Extensions.SecurityKey.Tests/UnitTest1.cs create mode 100644 test/Directory.Build.props diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c85887d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,52 @@ +# EditorConfig: https://EditorConfig.org + +root = true + +# All Files +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +# XML Configuration Files +[*.{xml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct,refactorlog,runsettings}] +indent_size = 2 + +# JSON Files +[*.{json,json5,webmanifest}] +indent_size = 2 + +# Project Files +[*.{csproj,sqlproj}] +indent_size = 2 + +# YAML Files +[*.{yml,yaml}] +indent_size = 2 + +# Markdown Files +[*.md] +trim_trailing_whitespace = false + +# Web Files +[*.{htm,html,js,jsm,ts,tsx,css,sass,scss,less,pcss,svg,vue}] +indent_size = 2 + +# Batch Files +[*.{cmd,bat}] +end_of_line = crlf + +# Bash Files +[*.sh] +end_of_line = lf + +[*.{cs,vb}] +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = true +dotnet_style_namespace_match_folder = true + +[*.cs] +csharp_using_directive_placement = outside_namespace +csharp_style_namespace_declarations = file_scoped:warning \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8885fa6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: daily + time: "01:00" + open-pull-requests-limit: 10 + +- package-ecosystem: nuget + directory: "/" + schedule: + interval: daily + time: "11:00" + open-pull-requests-limit: 10 diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..6d3ee30 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,99 @@ +name: Build + +env: + DOTNET_NOLOGO: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_ENVIRONMENT: github + ASPNETCORE_ENVIRONMENT: github + BUILD_PATH: "${{github.workspace}}/artifacts" + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + +on: + push: + branches: + - main + - develop + tags: + - "v*" + pull_request: + branches: + - main + - develop + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install .NET Core + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x + + - name: Restore Dependencies + run: dotnet restore + + - name: Build Solution + run: dotnet build --no-restore --configuration Release + + - name: Run Test + run: dotnet test --no-build --configuration Release --collect:"XPlat Code Coverage" --settings coverlet.runsettings + + - name: Report Coverage + if: success() + uses: coverallsapp/github-action@v2 + with: + file: "${{github.workspace}}/test/*/TestResults/*/coverage.info" + format: lcov + + - name: Create Packages + if: success() && github.event_name != 'pull_request' + run: dotnet pack --configuration Release --include-symbols --include-source --no-build --output "${{env.BUILD_PATH}}" + + - name: Upload Packages + if: success() && github.event_name != 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: packages + path: "${{env.BUILD_PATH}}" + + deploy: + runs-on: ubuntu-latest + needs: build + if: success() && github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) + + steps: + - name: Download Artifact + uses: actions/download-artifact@v4 + with: + name: packages + + - name: Publish Packages GitHub + run: | + for package in $(find -name "*.nupkg"); do + echo "${0##*/}": Pushing $package... + dotnet nuget push $package --source https://nuget.pkg.github.com/loresoft/index.json --api-key ${{ secrets.GITHUB_TOKEN }} --skip-duplicate + done + + - name: Publish Packages feedz + run: | + for package in $(find -name "*.nupkg"); do + echo "${0##*/}": Pushing $package... + dotnet nuget push $package --source https://f.feedz.io/loresoft/open/nuget/index.json --api-key ${{ secrets.FEEDDZ_KEY }} --skip-duplicate + done + + - name: Publish Packages Nuget + if: startsWith(github.ref, 'refs/tags/v') + run: | + for package in $(find -name "*.nupkg"); do + echo "${0##*/}": Pushing $package... + dotnet nuget push $package --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_KEY }} --skip-duplicate + done diff --git a/AspNetCore.Extensions.sln b/AspNetCore.Extensions.sln new file mode 100644 index 0000000..ed20d85 --- /dev/null +++ b/AspNetCore.Extensions.sln @@ -0,0 +1,92 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCore.Extensions.SecurityKey", "src\AspNetCore.Extensions.SecurityKey\AspNetCore.Extensions.SecurityKey.csproj", "{B6EAF1BD-410D-44F2-B909-448E37BC0748}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCore.Extensions.Diagnostics", "src\AspNetCore.Extensions.Diagnostics\AspNetCore.Extensions.Diagnostics.csproj", "{D556675A-4D8F-4F7B-A82C-7DC8CC255FE3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{8F6AF28D-3302-4CD2-93F8-FEDFF40DA284}" + ProjectSection(SolutionItems) = preProject + test\Directory.Build.props = test\Directory.Build.props + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCore.Extensions.SecurityKey.Tests", "test\AspNetCore.Extensions.SecurityKey.Tests\AspNetCore.Extensions.SecurityKey.Tests.csproj", "{524DA054-BB33-46BC-B906-1781DA92816D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCore.Extensions.Diagnostics.Tests", "test\AspNetCore.Extensions.Diagnostics.Tests\AspNetCore.Extensions.Diagnostics.Tests.csproj", "{2CF093CB-B64C-4735-BC63-B37DA06B0247}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{8FF17A69-FF3C-4F04-9F5A-1C58B0498D0D}" + ProjectSection(SolutionItems) = preProject + src\Directory.Build.props = src\Directory.Build.props + .github\workflows\dotnet.yml = .github\workflows\dotnet.yml + README.md = README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{8A309522-314D-4C40-9D0E-C42E88CB64E5}" + ProjectSection(SolutionItems) = preProject + samples\Directory.Build.props = samples\Directory.Build.props + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Controllers", "samples\Sample.Controllers\Sample.Controllers.csproj", "{24245841-8372-442D-8212-A357138445D9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.MinimalApi", "samples\Sample.MinimalApi\Sample.MinimalApi.csproj", "{21060613-AFA4-4C8E-B0C9-2CE4368DA6F2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Middleware", "samples\Sample.Middleware\Sample.Middleware.csproj", "{53460F71-F58D-4BEF-9441-2CCA82FA9415}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Shared", "samples\Sample.Shared\Sample.Shared.csproj", "{CF5ED041-2229-4481-8DCC-8BB4438B222C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B6EAF1BD-410D-44F2-B909-448E37BC0748}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6EAF1BD-410D-44F2-B909-448E37BC0748}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6EAF1BD-410D-44F2-B909-448E37BC0748}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6EAF1BD-410D-44F2-B909-448E37BC0748}.Release|Any CPU.Build.0 = Release|Any CPU + {D556675A-4D8F-4F7B-A82C-7DC8CC255FE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D556675A-4D8F-4F7B-A82C-7DC8CC255FE3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D556675A-4D8F-4F7B-A82C-7DC8CC255FE3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D556675A-4D8F-4F7B-A82C-7DC8CC255FE3}.Release|Any CPU.Build.0 = Release|Any CPU + {524DA054-BB33-46BC-B906-1781DA92816D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {524DA054-BB33-46BC-B906-1781DA92816D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {524DA054-BB33-46BC-B906-1781DA92816D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {524DA054-BB33-46BC-B906-1781DA92816D}.Release|Any CPU.Build.0 = Release|Any CPU + {2CF093CB-B64C-4735-BC63-B37DA06B0247}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2CF093CB-B64C-4735-BC63-B37DA06B0247}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2CF093CB-B64C-4735-BC63-B37DA06B0247}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2CF093CB-B64C-4735-BC63-B37DA06B0247}.Release|Any CPU.Build.0 = Release|Any CPU + {24245841-8372-442D-8212-A357138445D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {24245841-8372-442D-8212-A357138445D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {24245841-8372-442D-8212-A357138445D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {24245841-8372-442D-8212-A357138445D9}.Release|Any CPU.Build.0 = Release|Any CPU + {21060613-AFA4-4C8E-B0C9-2CE4368DA6F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21060613-AFA4-4C8E-B0C9-2CE4368DA6F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21060613-AFA4-4C8E-B0C9-2CE4368DA6F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21060613-AFA4-4C8E-B0C9-2CE4368DA6F2}.Release|Any CPU.Build.0 = Release|Any CPU + {53460F71-F58D-4BEF-9441-2CCA82FA9415}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53460F71-F58D-4BEF-9441-2CCA82FA9415}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53460F71-F58D-4BEF-9441-2CCA82FA9415}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53460F71-F58D-4BEF-9441-2CCA82FA9415}.Release|Any CPU.Build.0 = Release|Any CPU + {CF5ED041-2229-4481-8DCC-8BB4438B222C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF5ED041-2229-4481-8DCC-8BB4438B222C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF5ED041-2229-4481-8DCC-8BB4438B222C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF5ED041-2229-4481-8DCC-8BB4438B222C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {524DA054-BB33-46BC-B906-1781DA92816D} = {8F6AF28D-3302-4CD2-93F8-FEDFF40DA284} + {2CF093CB-B64C-4735-BC63-B37DA06B0247} = {8F6AF28D-3302-4CD2-93F8-FEDFF40DA284} + {24245841-8372-442D-8212-A357138445D9} = {8A309522-314D-4C40-9D0E-C42E88CB64E5} + {21060613-AFA4-4C8E-B0C9-2CE4368DA6F2} = {8A309522-314D-4C40-9D0E-C42E88CB64E5} + {53460F71-F58D-4BEF-9441-2CCA82FA9415} = {8A309522-314D-4C40-9D0E-C42E88CB64E5} + {CF5ED041-2229-4481-8DCC-8BB4438B222C} = {8A309522-314D-4C40-9D0E-C42E88CB64E5} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {E757B21F-53C0-43CE-94E6-65ADEC81158F} + EndGlobalSection +EndGlobal diff --git a/coverlet.runsettings b/coverlet.runsettings new file mode 100644 index 0000000..4eac22c --- /dev/null +++ b/coverlet.runsettings @@ -0,0 +1,14 @@ + + + + + + + lcov + Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute,TestSDKAutoGeneratedCode + true + + + + + diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6482efc7bd1380ad1813f8741f9d4abd48dbce0f GIT binary patch literal 5007 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 literal 0 HcmV?d00001 diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props new file mode 100644 index 0000000..598cb25 --- /dev/null +++ b/samples/Directory.Build.props @@ -0,0 +1,38 @@ + + + + Sample Project + Copyright © $([System.DateTime]::Now.ToString(yyyy)) LoreSoft + LoreSoft + en-US + false + + + + portable + true + true + snupkg + + + + latest + enable + 1591 + + + + v + + + + + + + + + + + + + diff --git a/samples/Sample.Controllers/Controllers.http b/samples/Sample.Controllers/Controllers.http new file mode 100644 index 0000000..d5548de --- /dev/null +++ b/samples/Sample.Controllers/Controllers.http @@ -0,0 +1,18 @@ +@HostAddress = http://localhost:5154 + +### + +GET {{HostAddress}}/weather/ +Accept: application/json + +### + +GET {{HostAddress}}/user/ +Accept: application/json +X-API-KEY: 01HSGVAYZSP9ZY219BCB7B7DA1 + +### + +GET {{HostAddress}}/address?X-API-KEY=01HSGVAYZSP9ZY219BCB7B7DA1 +Accept: application/json + diff --git a/samples/Sample.Controllers/Controllers/AddressController.cs b/samples/Sample.Controllers/Controllers/AddressController.cs new file mode 100644 index 0000000..8d9f812 --- /dev/null +++ b/samples/Sample.Controllers/Controllers/AddressController.cs @@ -0,0 +1,20 @@ +using AspNetCore.Extensions.SecurityKey; + +using Microsoft.AspNetCore.Mvc; + +using Sample.Shared; + +namespace Sample.Controllers.Controllers; + +[ApiController] +[Route("[controller]")] +public class AddressController : ControllerBase +{ + [SecurityKey] + [HttpGet(Name = "GetAddresses")] + public IEnumerable
Get() + { + return AddressFaker.Instance.Generate(5); + } + +} diff --git a/samples/Sample.Controllers/Controllers/UserController.cs b/samples/Sample.Controllers/Controllers/UserController.cs new file mode 100644 index 0000000..9917672 --- /dev/null +++ b/samples/Sample.Controllers/Controllers/UserController.cs @@ -0,0 +1,20 @@ +using AspNetCore.Extensions.SecurityKey; + +using Microsoft.AspNetCore.Mvc; + +using Sample.Shared; + +namespace Sample.Controllers.Controllers; + +[ApiController] +[Route("[controller]")] +[SecurityKey] +public class UserController : ControllerBase +{ + [HttpGet(Name = "GetUsers")] + public IEnumerable Get() + { + return UserFaker.Instance.Generate(5); + } + +} diff --git a/samples/Sample.Controllers/Controllers/WeatherController.cs b/samples/Sample.Controllers/Controllers/WeatherController.cs new file mode 100644 index 0000000..5fd01e7 --- /dev/null +++ b/samples/Sample.Controllers/Controllers/WeatherController.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; + +using Sample.Shared; + +namespace Sample.Controllers.Controllers; + +[ApiController] +[Route("[controller]")] +public class WeatherController : ControllerBase +{ + [HttpGet(Name = "GetWeather")] + public IEnumerable Get() + { + return WeatherFaker.Instance.Generate(5); + } +} diff --git a/samples/Sample.Controllers/Program.cs b/samples/Sample.Controllers/Program.cs new file mode 100644 index 0000000..22172a9 --- /dev/null +++ b/samples/Sample.Controllers/Program.cs @@ -0,0 +1,31 @@ +using AspNetCore.Extensions.SecurityKey; + +namespace Sample.Controllers; + +public static class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddControllers(); + + builder.Services.AddSecurityKey(options => options.ConfigurationName = "Authentication:ApiKey"); + + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + + var app = builder.Build(); + + app.UseSwagger(); + app.UseSwaggerUI(); + + app.UseHttpsRedirection(); + + app.UseAuthorization(); + + app.MapControllers(); + + app.Run(); + } +} diff --git a/samples/Sample.Controllers/Properties/launchSettings.json b/samples/Sample.Controllers/Properties/launchSettings.json new file mode 100644 index 0000000..a1122a7 --- /dev/null +++ b/samples/Sample.Controllers/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Sample.Controllers": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7236;http://localhost:5154", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/Sample.Controllers/Sample.Controllers.csproj b/samples/Sample.Controllers/Sample.Controllers.csproj new file mode 100644 index 0000000..e76c294 --- /dev/null +++ b/samples/Sample.Controllers/Sample.Controllers.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/samples/Sample.Controllers/appsettings.json b/samples/Sample.Controllers/appsettings.json new file mode 100644 index 0000000..3173025 --- /dev/null +++ b/samples/Sample.Controllers/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Authentication": { + "ApiKey": "01HSGVAYZSP9ZY219BCB7B7DA1" + }, + "AllowedHosts": "*" +} diff --git a/samples/Sample.Middleware/Middleware.http b/samples/Sample.Middleware/Middleware.http new file mode 100644 index 0000000..e5e977c --- /dev/null +++ b/samples/Sample.Middleware/Middleware.http @@ -0,0 +1,19 @@ +@HostAddress = http://localhost:5284 + +### + +GET {{HostAddress}}/weather/ +Accept: application/json +X-API-KEY: 01HSGVAH2M5WVQYG4YPT7FNK4K8 + +### + +GET {{HostAddress}}/users/ +Accept: application/json +x-api-key: 01HSGVAH2M5WVQYG4YPT7FNK4K8 + +### + +GET {{HostAddress}}/addresses/ +Accept: application/json +X-API-KEY: 01hsgvah2m5wvqyg4ypt7fnk4k8 diff --git a/samples/Sample.Middleware/Program.cs b/samples/Sample.Middleware/Program.cs new file mode 100644 index 0000000..30b70d7 --- /dev/null +++ b/samples/Sample.Middleware/Program.cs @@ -0,0 +1,46 @@ +using AspNetCore.Extensions.SecurityKey; + +using Sample.Shared; + +namespace Sample.Middleware; + +public static class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddAuthorization(); + + builder.Services.AddSecurityKey(); + + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + + var app = builder.Build(); + + app.UseSwagger(); + app.UseSwaggerUI(); + + // required api key for all end points + app.UseSecurityKey(); + + app.UseHttpsRedirection(); + + app.UseAuthorization(); + + app.MapGet("/weather", () => WeatherFaker.Instance.Generate(5)) + .WithName("GetWeatherForecast") + .WithOpenApi(); + + app.MapGet("/users", () => UserFaker.Instance.Generate(10)) + .WithName("GetUsers") + .WithOpenApi(); + + app.MapGet("/addresses", () => AddressFaker.Instance.Generate(10)) + .WithName("GetAddresses") + .WithOpenApi(); + + app.Run(); + } +} diff --git a/samples/Sample.Middleware/Properties/launchSettings.json b/samples/Sample.Middleware/Properties/launchSettings.json new file mode 100644 index 0000000..47db82b --- /dev/null +++ b/samples/Sample.Middleware/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Sample.Middleware": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7006;http://localhost:5284", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/Sample.Middleware/Sample.Middleware.csproj b/samples/Sample.Middleware/Sample.Middleware.csproj new file mode 100644 index 0000000..e344402 --- /dev/null +++ b/samples/Sample.Middleware/Sample.Middleware.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + diff --git a/samples/Sample.Middleware/appsettings.json b/samples/Sample.Middleware/appsettings.json new file mode 100644 index 0000000..ed06e52 --- /dev/null +++ b/samples/Sample.Middleware/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "SecurityKey": "01HSGVAH2M5WVQYG4YPT7FNK4K8", + "AllowedHosts": "*" +} diff --git a/samples/Sample.MinimalApi/MinimalApi.http b/samples/Sample.MinimalApi/MinimalApi.http new file mode 100644 index 0000000..5415ea2 --- /dev/null +++ b/samples/Sample.MinimalApi/MinimalApi.http @@ -0,0 +1,18 @@ +@HostAddress = http://localhost:5009 + +### + +GET {{HostAddress}}/weather/ +Accept: application/json + +### + +GET {{HostAddress}}/users/ +Accept: application/json +X-API-KEY: 01HSGVBSF99SK6XMJQJYF0X3WQ + +### + +GET {{HostAddress}}/addresses/ +Accept: application/json +X-API-KEY: 01HSGVBSF99SK6XMJQJYF0X3WQ diff --git a/samples/Sample.MinimalApi/Program.cs b/samples/Sample.MinimalApi/Program.cs new file mode 100644 index 0000000..18e1fc1 --- /dev/null +++ b/samples/Sample.MinimalApi/Program.cs @@ -0,0 +1,50 @@ +using AspNetCore.Extensions.SecurityKey; + +using Sample.Shared; + +namespace Sample.MinimalApi; + +public static class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + builder.Services + .AddAuthentication() + .AddSecurityKey(); + + builder.Services.AddAuthorization(); + + builder.Services.AddSecurityKey(); + + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + + var app = builder.Build(); + + app.UseSwagger(); + app.UseSwaggerUI(); + + app.UseHttpsRedirection(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.MapGet("/weather", () => WeatherFaker.Instance.Generate(5)) + .WithName("GetWeatherForecast") + .WithOpenApi(); + + app.MapGet("/users", () => UserFaker.Instance.Generate(10)) + .WithName("GetUsers") + .WithOpenApi() + .RequireSecurityKey(); + + app.MapGet("/addresses", () => AddressFaker.Instance.Generate(10)) + .WithName("GetAddresses") + .WithOpenApi() + .RequireAuthorization(); + + app.Run(); + } +} diff --git a/samples/Sample.MinimalApi/Properties/launchSettings.json b/samples/Sample.MinimalApi/Properties/launchSettings.json new file mode 100644 index 0000000..f3e8fa6 --- /dev/null +++ b/samples/Sample.MinimalApi/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Sample.MinimalApi": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7216;http://localhost:5009", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/Sample.MinimalApi/Sample.MinimalApi.csproj b/samples/Sample.MinimalApi/Sample.MinimalApi.csproj new file mode 100644 index 0000000..e344402 --- /dev/null +++ b/samples/Sample.MinimalApi/Sample.MinimalApi.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + diff --git a/samples/Sample.MinimalApi/appsettings.json b/samples/Sample.MinimalApi/appsettings.json new file mode 100644 index 0000000..c9d6e67 --- /dev/null +++ b/samples/Sample.MinimalApi/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "SecurityKey": "01HSGVBGWXWDWTFGTJSYFXXDXQ;01HSGVBSF99SK6XMJQJYF0X3WQ", + "AllowedHosts": "*" +} diff --git a/samples/Sample.Shared/Address.cs b/samples/Sample.Shared/Address.cs new file mode 100644 index 0000000..87c3ec8 --- /dev/null +++ b/samples/Sample.Shared/Address.cs @@ -0,0 +1,3 @@ +namespace Sample.Shared; + +public record Address(string Line1, string Line2, string City, string State, string ZipCode); diff --git a/samples/Sample.Shared/AddressFaker.cs b/samples/Sample.Shared/AddressFaker.cs new file mode 100644 index 0000000..2ca9a3b --- /dev/null +++ b/samples/Sample.Shared/AddressFaker.cs @@ -0,0 +1,20 @@ +namespace Sample.Shared; + +public class AddressFaker : Faker
+{ + public AddressFaker() + { + CustomInstantiator(f => new Address( + Line1: f.Address.StreetAddress(), + Line2: f.Address.SecondaryAddress(), + City: f.Address.City(), + State: f.Address.State(), + ZipCode: f.Address.ZipCode() + ) + ); + } + + private static readonly Lazy _instance = new(); + + public static AddressFaker Instance => _instance.Value; +} diff --git a/samples/Sample.Shared/Sample.Shared.csproj b/samples/Sample.Shared/Sample.Shared.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/samples/Sample.Shared/Sample.Shared.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/samples/Sample.Shared/User.cs b/samples/Sample.Shared/User.cs new file mode 100644 index 0000000..e3d3c83 --- /dev/null +++ b/samples/Sample.Shared/User.cs @@ -0,0 +1,3 @@ +namespace Sample.Shared; + +public record User(string First, string Last, string Email); diff --git a/samples/Sample.Shared/UserFaker.cs b/samples/Sample.Shared/UserFaker.cs new file mode 100644 index 0000000..949ffec --- /dev/null +++ b/samples/Sample.Shared/UserFaker.cs @@ -0,0 +1,18 @@ +namespace Sample.Shared; + +public class UserFaker : Faker +{ + public UserFaker() + { + CustomInstantiator(f => new User( + First: f.Name.FirstName(), + Last: f.Name.LastName(), + Email: f.Internet.Email() + ) + ); + } + + private static readonly Lazy _instance = new(); + + public static UserFaker Instance => _instance.Value; +} diff --git a/samples/Sample.Shared/Weather.cs b/samples/Sample.Shared/Weather.cs new file mode 100644 index 0000000..4a470fd --- /dev/null +++ b/samples/Sample.Shared/Weather.cs @@ -0,0 +1,12 @@ +namespace Sample.Shared; + +public class Weather +{ + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } +} diff --git a/samples/Sample.Shared/WeatherFaker.cs b/samples/Sample.Shared/WeatherFaker.cs new file mode 100644 index 0000000..ec20b3b --- /dev/null +++ b/samples/Sample.Shared/WeatherFaker.cs @@ -0,0 +1,16 @@ +namespace Sample.Shared; + +public class WeatherFaker : Faker +{ + public WeatherFaker() + { + RuleFor(o => o.Date, f => DateOnly.FromDateTime(DateTime.Now.AddDays(f.IndexFaker))); + RuleFor(o => o.TemperatureC, f => f.Random.Number(-20, 55)); + RuleFor(o => o.Summary, f => f.Random.ArrayElement(["Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"])); + } + + private static readonly Lazy _instance = new(); + + public static WeatherFaker Instance => _instance.Value; + +} diff --git a/src/AspNetCore.Extensions.Diagnostics/AspNetCore.Extensions.Diagnostics.csproj b/src/AspNetCore.Extensions.Diagnostics/AspNetCore.Extensions.Diagnostics.csproj new file mode 100644 index 0000000..b05a3f0 --- /dev/null +++ b/src/AspNetCore.Extensions.Diagnostics/AspNetCore.Extensions.Diagnostics.csproj @@ -0,0 +1,12 @@ + + + + net6.0;net7.0;net8.0 + enable + enable + + + + + + diff --git a/src/AspNetCore.Extensions.Diagnostics/Class1.cs b/src/AspNetCore.Extensions.Diagnostics/Class1.cs new file mode 100644 index 0000000..ba74193 --- /dev/null +++ b/src/AspNetCore.Extensions.Diagnostics/Class1.cs @@ -0,0 +1,6 @@ +namespace AspNetCore.Extensions.Diagnostics; + +public class Class1 +{ + +} diff --git a/src/AspNetCore.Extensions.SecurityKey/ApplicationBuilderExtensions.cs b/src/AspNetCore.Extensions.SecurityKey/ApplicationBuilderExtensions.cs new file mode 100644 index 0000000..21c4c4b --- /dev/null +++ b/src/AspNetCore.Extensions.SecurityKey/ApplicationBuilderExtensions.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Builder; + +namespace AspNetCore.Extensions.SecurityKey; + +public static class ApplicationBuilderExtensions +{ + public static IApplicationBuilder UseSecurityKey(this IApplicationBuilder app) + { + return app.UseMiddleware(); + } +} diff --git a/src/AspNetCore.Extensions.SecurityKey/AspNetCore.Extensions.SecurityKey.csproj b/src/AspNetCore.Extensions.SecurityKey/AspNetCore.Extensions.SecurityKey.csproj new file mode 100644 index 0000000..99cc5c6 --- /dev/null +++ b/src/AspNetCore.Extensions.SecurityKey/AspNetCore.Extensions.SecurityKey.csproj @@ -0,0 +1,13 @@ + + + + net6.0;net7.0;net8.0 + enable + enable + + + + + + + diff --git a/src/AspNetCore.Extensions.SecurityKey/AuthenticationBuilderExtensions.cs b/src/AspNetCore.Extensions.SecurityKey/AuthenticationBuilderExtensions.cs new file mode 100644 index 0000000..bb5c77a --- /dev/null +++ b/src/AspNetCore.Extensions.SecurityKey/AuthenticationBuilderExtensions.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Authentication; + +namespace AspNetCore.Extensions.SecurityKey; + +public static class AuthenticationBuilderExtensions +{ + public static AuthenticationBuilder AddSecurityKey(this AuthenticationBuilder builder) + { + return builder.AddScheme( + SecurityKeyAuthenticationDefaults.AuthenticationScheme, + options => { } + ); + } +} diff --git a/src/AspNetCore.Extensions.SecurityKey/DependencyInjectionExtensions.cs b/src/AspNetCore.Extensions.SecurityKey/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..a4efee0 --- /dev/null +++ b/src/AspNetCore.Extensions.SecurityKey/DependencyInjectionExtensions.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; + +namespace AspNetCore.Extensions.SecurityKey; + +public static class DependencyInjectionExtensions +{ + public static IServiceCollection AddSecurityKey(this IServiceCollection services, Action? configure = null) + { + services.AddHttpContextAccessor(); + + services.AddOptions(); + if (configure != null) + services.Configure(configure); + + services.TryAddSingleton(); + services.TryAddSingleton(); + + // used by SecurityKeyAttribute + services.TryAddSingleton(); + + return services; + } +} diff --git a/src/AspNetCore.Extensions.SecurityKey/EndpointFilterExtensions.cs b/src/AspNetCore.Extensions.SecurityKey/EndpointFilterExtensions.cs new file mode 100644 index 0000000..08cb6cf --- /dev/null +++ b/src/AspNetCore.Extensions.SecurityKey/EndpointFilterExtensions.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace AspNetCore.Extensions.SecurityKey; + +public static class EndpointFilterExtensions +{ + #if NET7_0_OR_GREATER + public static RouteHandlerBuilder RequireSecurityKey(this RouteHandlerBuilder builder) + { + builder.AddEndpointFilter(); + return builder; + } + #endif +} diff --git a/src/AspNetCore.Extensions.SecurityKey/ISecurityKeyExtractor.cs b/src/AspNetCore.Extensions.SecurityKey/ISecurityKeyExtractor.cs new file mode 100644 index 0000000..8ffa391 --- /dev/null +++ b/src/AspNetCore.Extensions.SecurityKey/ISecurityKeyExtractor.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Http; + +namespace AspNetCore.Extensions.SecurityKey; + +public interface ISecurityKeyExtractor +{ + string? GetKey(HttpContext? context); +} diff --git a/src/AspNetCore.Extensions.SecurityKey/ISecurityKeyValidator.cs b/src/AspNetCore.Extensions.SecurityKey/ISecurityKeyValidator.cs new file mode 100644 index 0000000..af6e989 --- /dev/null +++ b/src/AspNetCore.Extensions.SecurityKey/ISecurityKeyValidator.cs @@ -0,0 +1,6 @@ +namespace AspNetCore.Extensions.SecurityKey; + +public interface ISecurityKeyValidator +{ + bool Validate(string? value); +} diff --git a/src/AspNetCore.Extensions.SecurityKey/SecurityKeyAttribute.cs b/src/AspNetCore.Extensions.SecurityKey/SecurityKeyAttribute.cs new file mode 100644 index 0000000..aa16b01 --- /dev/null +++ b/src/AspNetCore.Extensions.SecurityKey/SecurityKeyAttribute.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc; + +namespace AspNetCore.Extensions.SecurityKey; + +public class SecurityKeyAttribute : ServiceFilterAttribute +{ + public SecurityKeyAttribute() + : base(typeof(SecurityKeyAuthorizationFilter)) + { + } +} diff --git a/src/AspNetCore.Extensions.SecurityKey/SecurityKeyAuthenticationDefaults.cs b/src/AspNetCore.Extensions.SecurityKey/SecurityKeyAuthenticationDefaults.cs new file mode 100644 index 0000000..021194a --- /dev/null +++ b/src/AspNetCore.Extensions.SecurityKey/SecurityKeyAuthenticationDefaults.cs @@ -0,0 +1,6 @@ +namespace AspNetCore.Extensions.SecurityKey; + +public static class SecurityKeyAuthenticationDefaults +{ + public const string AuthenticationScheme = "SecurityKey"; +} diff --git a/src/AspNetCore.Extensions.SecurityKey/SecurityKeyAuthenticationHandler.cs b/src/AspNetCore.Extensions.SecurityKey/SecurityKeyAuthenticationHandler.cs new file mode 100644 index 0000000..e93ce52 --- /dev/null +++ b/src/AspNetCore.Extensions.SecurityKey/SecurityKeyAuthenticationHandler.cs @@ -0,0 +1,51 @@ +using System.Security.Claims; +using System.Text.Encodings.Web; + +using AspNetCore.Extensions.Authentication; + +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace AspNetCore.Extensions.SecurityKey; + +public class SecurityKeyAuthenticationHandler : AuthenticationHandler +{ + private readonly ISecurityKeyExtractor _securityKeyExtractor; + private readonly ISecurityKeyValidator _securityKeyValidator; + +#pragma warning disable CS0618 // allow ISystemClock for compatibility + public SecurityKeyAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock, + ISecurityKeyExtractor securityKeyExtractor, + ISecurityKeyValidator securityKeyValidator) + : base(options, logger, encoder, clock) + { + _securityKeyExtractor = securityKeyExtractor; + _securityKeyValidator = securityKeyValidator; + } +#pragma warning restore CS0618 + + protected override Task HandleAuthenticateAsync() + { + var securityKey = _securityKeyExtractor.GetKey(Context); + + if (!_securityKeyValidator.Validate(securityKey)) + { + SecurityKeyLogger.InvalidSecurityKey(Logger, securityKey); + return Task.FromResult(AuthenticateResult.Fail("Invalid Security Key")); + } + + // create a user claim for the security key + var claims = new[] { new Claim(ClaimTypes.Name, "Security Key") }; + var identity = new ClaimsIdentity(claims, SecurityKeyAuthenticationDefaults.AuthenticationScheme); + var principal = new ClaimsPrincipal(identity); + + var ticket = new AuthenticationTicket(principal, Scheme.Name); + + return Task.FromResult(AuthenticateResult.Success(ticket)); + } +} diff --git a/src/AspNetCore.Extensions.SecurityKey/SecurityKeyAuthenticationSchemeOptions.cs b/src/AspNetCore.Extensions.SecurityKey/SecurityKeyAuthenticationSchemeOptions.cs new file mode 100644 index 0000000..7ae99e1 --- /dev/null +++ b/src/AspNetCore.Extensions.SecurityKey/SecurityKeyAuthenticationSchemeOptions.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Authentication; + +namespace AspNetCore.Extensions.SecurityKey; + +public class SecurityKeyAuthenticationSchemeOptions : AuthenticationSchemeOptions +{ + +} diff --git a/src/AspNetCore.Extensions.SecurityKey/SecurityKeyAuthorizationFilter.cs b/src/AspNetCore.Extensions.SecurityKey/SecurityKeyAuthorizationFilter.cs new file mode 100644 index 0000000..0381380 --- /dev/null +++ b/src/AspNetCore.Extensions.SecurityKey/SecurityKeyAuthorizationFilter.cs @@ -0,0 +1,39 @@ +using AspNetCore.Extensions.Authentication; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Logging; + +namespace AspNetCore.Extensions.SecurityKey; + +public class SecurityKeyAuthorizationFilter : IAuthorizationFilter +{ + private readonly ISecurityKeyExtractor _authenticationKeyExtractor; + private readonly ISecurityKeyValidator _authenticationKeyValidator; + private readonly ILogger _logger; + + public SecurityKeyAuthorizationFilter( + ISecurityKeyExtractor authenticationKeyExtractor, + ISecurityKeyValidator authenticationKeyValidator, + ILogger logger) + { + _authenticationKeyExtractor = authenticationKeyExtractor ?? throw new ArgumentNullException(nameof(authenticationKeyExtractor)); + _authenticationKeyValidator = authenticationKeyValidator ?? throw new ArgumentNullException(nameof(authenticationKeyValidator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public void OnAuthorization(AuthorizationFilterContext context) + { + if (context is null) + throw new ArgumentNullException(nameof(context)); + + var securityKey = _authenticationKeyExtractor.GetKey(context.HttpContext); + + if (_authenticationKeyValidator.Validate(securityKey)) + return; + + SecurityKeyLogger.InvalidSecurityKey(_logger, securityKey); + + context.Result = new UnauthorizedResult(); + } +} diff --git a/src/AspNetCore.Extensions.SecurityKey/SecurityKeyEndpointFilter.cs b/src/AspNetCore.Extensions.SecurityKey/SecurityKeyEndpointFilter.cs new file mode 100644 index 0000000..b0363ec --- /dev/null +++ b/src/AspNetCore.Extensions.SecurityKey/SecurityKeyEndpointFilter.cs @@ -0,0 +1,40 @@ +using AspNetCore.Extensions.Authentication; + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace AspNetCore.Extensions.SecurityKey; + +#if NET7_0_OR_GREATER +public class SecurityKeyEndpointFilter : IEndpointFilter +{ + private readonly ISecurityKeyExtractor _securityKeyExtractor; + private readonly ISecurityKeyValidator _securityKeyValidator; + private readonly ILogger _logger; + + public SecurityKeyEndpointFilter( + ISecurityKeyExtractor securityKeyExtractor, + ISecurityKeyValidator securityKeyValidator, + ILogger logger) + { + _securityKeyExtractor = securityKeyExtractor ?? throw new ArgumentNullException(nameof(securityKeyExtractor)); + _securityKeyValidator = securityKeyValidator ?? throw new ArgumentNullException(nameof(securityKeyValidator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + { + if (context is null) + throw new ArgumentNullException(nameof(context)); + + var securityKey = _securityKeyExtractor.GetKey(context.HttpContext); + + if (_securityKeyValidator.Validate(securityKey)) + return await next(context); + + SecurityKeyLogger.InvalidSecurityKey(_logger, securityKey); + + return Results.Unauthorized(); + } +} +#endif diff --git a/src/AspNetCore.Extensions.SecurityKey/SecurityKeyExtractor.cs b/src/AspNetCore.Extensions.SecurityKey/SecurityKeyExtractor.cs new file mode 100644 index 0000000..df11fff --- /dev/null +++ b/src/AspNetCore.Extensions.SecurityKey/SecurityKeyExtractor.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +namespace AspNetCore.Extensions.SecurityKey; + +public class SecurityKeyExtractor : ISecurityKeyExtractor +{ + private readonly SecurityKeyOptions _securityKeyOptions; + + public SecurityKeyExtractor(IOptions securityKeyOptions) + { + if (securityKeyOptions == null) + throw new ArgumentNullException(nameof(securityKeyOptions)); + + _securityKeyOptions = securityKeyOptions.Value; + } + + public string? GetKey(HttpContext? context) + { + if (context is null) + return null; + + if (context.Request.Headers.TryGetValue(_securityKeyOptions.HeaderName, out var headerKey)) + return headerKey; + + if (context.Request.Query.TryGetValue(_securityKeyOptions.QueryName, out var queryKey)) + return queryKey; + + if (context.Request.Cookies.TryGetValue(_securityKeyOptions.CookieName, out var cookieKey)) + return cookieKey; + + return null; + } +} diff --git a/src/AspNetCore.Extensions.SecurityKey/SecurityKeyLogger.cs b/src/AspNetCore.Extensions.SecurityKey/SecurityKeyLogger.cs new file mode 100644 index 0000000..7f4ca5b --- /dev/null +++ b/src/AspNetCore.Extensions.SecurityKey/SecurityKeyLogger.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.Logging; + +namespace AspNetCore.Extensions.Authentication; + +public partial class SecurityKeyLogger +{ + [LoggerMessage(1001, LogLevel.Error, "Invalid Security Key '{SecurityKey}'")] + public static partial void InvalidSecurityKey(ILogger logger, string? securityKey); + + [LoggerMessage(1002, LogLevel.Debug, "Using Security Keys '{SecurityKey}' from configuration '{ConfigurationName}'")] + public static partial void SecurityKeyUsage(ILogger logger, string? securityKey, string? configurationName); + +} diff --git a/src/AspNetCore.Extensions.SecurityKey/SecurityKeyMiddleware.cs b/src/AspNetCore.Extensions.SecurityKey/SecurityKeyMiddleware.cs new file mode 100644 index 0000000..1fb4a77 --- /dev/null +++ b/src/AspNetCore.Extensions.SecurityKey/SecurityKeyMiddleware.cs @@ -0,0 +1,47 @@ +using System.Net; + +using AspNetCore.Extensions.Authentication; + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace AspNetCore.Extensions.SecurityKey; + +public class SecurityKeyMiddleware +{ + private readonly RequestDelegate _next; + + private readonly ISecurityKeyExtractor _securityKeyExtractor; + private readonly ISecurityKeyValidator _securityKeyValidator; + private readonly ILogger _logger; + + public SecurityKeyMiddleware( + RequestDelegate next, + ISecurityKeyExtractor securityKeyExtractor, + ISecurityKeyValidator securityKeyValidator, + ILogger logger) + { + _next = next; + _securityKeyExtractor = securityKeyExtractor ?? throw new ArgumentNullException(nameof(securityKeyExtractor)); + _securityKeyValidator = securityKeyValidator ?? throw new ArgumentNullException(nameof(securityKeyValidator)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task InvokeAsync(HttpContext context) + { + if (context is null) + throw new ArgumentNullException(nameof(context)); + + var securityKey = _securityKeyExtractor.GetKey(context); + + if (_securityKeyValidator.Validate(securityKey)) + { + await _next(context).ConfigureAwait(false); + return; + } + + SecurityKeyLogger.InvalidSecurityKey(_logger, securityKey); + + context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + } +} diff --git a/src/AspNetCore.Extensions.SecurityKey/SecurityKeyOptions.cs b/src/AspNetCore.Extensions.SecurityKey/SecurityKeyOptions.cs new file mode 100644 index 0000000..00a1a0e --- /dev/null +++ b/src/AspNetCore.Extensions.SecurityKey/SecurityKeyOptions.cs @@ -0,0 +1,14 @@ +namespace AspNetCore.Extensions.SecurityKey; + +public class SecurityKeyOptions +{ + public string HeaderName { get; set; } = "x-api-key"; + + public string QueryName { get; set; } = "x-api-key"; + + public string CookieName { get; set; } = "x-api-key"; + + public string ConfigurationName { get; set; } = "SecurityKey"; + + public IEqualityComparer KeyComparer { get; set; } = StringComparer.OrdinalIgnoreCase; +} diff --git a/src/AspNetCore.Extensions.SecurityKey/SecurityKeyValidator.cs b/src/AspNetCore.Extensions.SecurityKey/SecurityKeyValidator.cs new file mode 100644 index 0000000..0535124 --- /dev/null +++ b/src/AspNetCore.Extensions.SecurityKey/SecurityKeyValidator.cs @@ -0,0 +1,49 @@ +using AspNetCore.Extensions.Authentication; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace AspNetCore.Extensions.SecurityKey; + +public class SecurityKeyValidator : ISecurityKeyValidator +{ + private readonly IConfiguration _configuration; + private readonly SecurityKeyOptions _securityKeyOptions; + private readonly ILogger _logger; + + private readonly Lazy> _validKeys; + + public SecurityKeyValidator( + IConfiguration configuration, + IOptions securityKeyOptions, + ILogger logger) + { + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + _securityKeyOptions = securityKeyOptions?.Value ?? throw new ArgumentNullException(nameof(securityKeyOptions)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + // one-time extract keys + _validKeys = new Lazy>(ExractKeys); + } + + + public bool Validate(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return false; + + return _validKeys.Value.Contains(value); + } + + private HashSet ExractKeys() + { + var keyString = _configuration.GetValue(_securityKeyOptions.ConfigurationName) ?? string.Empty; + + SecurityKeyLogger.SecurityKeyUsage(_logger, keyString, _securityKeyOptions.ConfigurationName); + + var keys = keyString.Split([";", ","], StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) ?? []; + + return new HashSet(keys, _securityKeyOptions.KeyComparer); + } +} diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..3752fbe --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,54 @@ + + + + ASP.NET Core Extension Projects + Copyright © $([System.DateTime]::Now.ToString(yyyy)) LoreSoft + LoreSoft + en-US + true + asp.net;aspnetcore;apikey + https://github.com/loresoft/AspNetCore.Extensions + MIT + logo.png + README.md + git + https://github.com/loresoft/AspNetCore.Extensions + true + + + + portable + true + true + snupkg + + + + latest + enable + 1591 + + + + v + + + + + + + + + + true + \ + false + + + true + \ + false + + + + diff --git a/test/AspNetCore.Extensions.Diagnostics.Tests/AspNetCore.Extensions.Diagnostics.Tests.csproj b/test/AspNetCore.Extensions.Diagnostics.Tests/AspNetCore.Extensions.Diagnostics.Tests.csproj new file mode 100644 index 0000000..4ee8008 --- /dev/null +++ b/test/AspNetCore.Extensions.Diagnostics.Tests/AspNetCore.Extensions.Diagnostics.Tests.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + enable + + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/test/AspNetCore.Extensions.Diagnostics.Tests/UnitTest1.cs b/test/AspNetCore.Extensions.Diagnostics.Tests/UnitTest1.cs new file mode 100644 index 0000000..3a59c6f --- /dev/null +++ b/test/AspNetCore.Extensions.Diagnostics.Tests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace AspNetCore.Extensions.Diagnostics.Tests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} \ No newline at end of file diff --git a/test/AspNetCore.Extensions.SecurityKey.Tests/AspNetCore.Extensions.SecurityKey.Tests.csproj b/test/AspNetCore.Extensions.SecurityKey.Tests/AspNetCore.Extensions.SecurityKey.Tests.csproj new file mode 100644 index 0000000..04022e7 --- /dev/null +++ b/test/AspNetCore.Extensions.SecurityKey.Tests/AspNetCore.Extensions.SecurityKey.Tests.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + enable + + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/test/AspNetCore.Extensions.SecurityKey.Tests/UnitTest1.cs b/test/AspNetCore.Extensions.SecurityKey.Tests/UnitTest1.cs new file mode 100644 index 0000000..b9d4ca7 --- /dev/null +++ b/test/AspNetCore.Extensions.SecurityKey.Tests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace AspNetCore.Extensions.Authentication.Tests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} \ No newline at end of file diff --git a/test/Directory.Build.props b/test/Directory.Build.props new file mode 100644 index 0000000..8d41cc4 --- /dev/null +++ b/test/Directory.Build.props @@ -0,0 +1,46 @@ + + + + Test Project + Copyright © $([System.DateTime]::Now.ToString(yyyy)) LoreSoft + LoreSoft + en-US + false + + + + portable + true + true + snupkg + + + + latest + enable + 1591 + + + + v + + + + + + + + + + + + + + + + + + + + +