From 90cdab5d7ee8e6607608a33815313c1ebbff0210 Mon Sep 17 00:00:00 2001 From: "Shkar T. Noori" Date: Mon, 6 Mar 2023 13:20:35 +0300 Subject: [PATCH] First commit --- .editorconfig | 206 ++++++++ .github/dependabot.yml | 20 + .github/workflows/codeql-analysis.yml | 36 ++ .github/workflows/deploy-nuget.yaml | 44 ++ .github/workflows/sonarqube.yaml | 51 ++ .github/workflows/tests-base.yaml | 33 ++ .gitignore | 477 ++++++++++++++++++ DIT.Authentication.sln | 27 + src/GatewayAuth/Abstractions.cs | 21 + src/GatewayAuth/Defaults.cs | 15 + src/GatewayAuth/EmptyHeaderHandler.cs | 32 ++ .../Extensions/Base64JsonClaimProvider.cs | 33 ++ .../Extensions/GatewayAuthExtensions.cs | 96 ++++ src/GatewayAuth/GatewayAuth.csproj | 19 + src/GatewayAuth/GatewayAuthException.cs | 21 + src/GatewayAuth/Handler.cs | 84 +++ .../Base64JsonClaimsProvider.cs | 37 ++ .../CertificateSignatureValidator.cs | 33 ++ src/GatewayAuth/Options/Options.cs | 19 + .../Options/PostConfigureOptions.cs | 22 + 20 files changed, 1326 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/deploy-nuget.yaml create mode 100644 .github/workflows/sonarqube.yaml create mode 100644 .github/workflows/tests-base.yaml create mode 100644 .gitignore create mode 100644 DIT.Authentication.sln create mode 100644 src/GatewayAuth/Abstractions.cs create mode 100644 src/GatewayAuth/Defaults.cs create mode 100644 src/GatewayAuth/EmptyHeaderHandler.cs create mode 100644 src/GatewayAuth/Extensions/Base64JsonClaimProvider.cs create mode 100644 src/GatewayAuth/Extensions/GatewayAuthExtensions.cs create mode 100644 src/GatewayAuth/GatewayAuth.csproj create mode 100644 src/GatewayAuth/GatewayAuthException.cs create mode 100644 src/GatewayAuth/Handler.cs create mode 100644 src/GatewayAuth/Implementations/Base64JsonClaimsProvider.cs create mode 100644 src/GatewayAuth/Implementations/CertificateSignatureValidator.cs create mode 100644 src/GatewayAuth/Options/Options.cs create mode 100644 src/GatewayAuth/Options/PostConfigureOptions.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7f12cbd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,206 @@ +# To learn more about .editorconfig see https://aka.ms/editorconfigdocs +############################### +# Core EditorConfig Options # +############################### + +root = true + +# All files +[*] +indent_style = space +# Code files +[*.{cs}] +indent_size = 4 +insert_final_newline = true +charset = utf-8-bom + +############################### +# .NET Coding Conventions # +############################### +[*.{cs}] +# Organize usings +dotnet_sort_system_directives_first = true + +# this. preferences +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = true:suggestion + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:silent + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:error +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:error +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:error +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:error +dotnet_style_readonly_field = true:error + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion + +dotnet_style_explicit_tuple_names = true:error +dotnet_style_prefer_inferred_tuple_names = true:suggestion + +dotnet_style_null_propagation = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion + +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:silent + +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:silent + +############################### +# Naming Conventions # +############################### +# Symbols +# Naming Symbols +dotnet_naming_symbols.public_symbols.applicable_kinds = property,method,field,event,delegate +dotnet_naming_symbols.public_symbols.applicable_accessibilities = public + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +dotnet_naming_symbols.private_constants.applicable_kinds = field +dotnet_naming_symbols.private_constants.required_modifiers = const +dotnet_naming_symbols.private_constants.applicable_accessibilities = private + +# Style Definitions +dotnet_naming_style.pascal.capitalization = pascal_case + +dotnet_naming_style.uppercase.capitalization = all_upper + +dotnet_naming_style.pascal_starts_with_underscore.capitalization = pascal_case +dotnet_naming_style.pascal_starts_with_underscore.required_prefix = _ + +dotnet_naming_style.camel_starts_with_underscore.capitalization = camel_case +dotnet_naming_style.camel_starts_with_underscore.required_prefix = _ + +# Public members must be in pascal casing (public_members_pascal_case) +dotnet_naming_rule.public_members_pascal_case.symbols = public_symbols +dotnet_naming_rule.public_members_pascal_case.style = pascal +dotnet_naming_rule.public_members_pascal_case.severity = error + +# Private constants must be in uppercase (private_constants_must_be_uppercase) +dotnet_naming_rule.private_constants_must_be_uppercase.symbols = private_constants +dotnet_naming_rule.private_constants_must_be_uppercase.style = uppercase +dotnet_naming_rule.private_constants_must_be_uppercase.severity = error + +# Private fields must be start with an underscore (private_fields_must_start_with_underscore) +dotnet_naming_rule.private_fields_must_start_with_underscore.symbols = private_fields +dotnet_naming_rule.private_fields_must_start_with_underscore.style = camel_starts_with_underscore +dotnet_naming_rule.private_fields_must_start_with_underscore.severity = error + +############################### +# C# Coding Conventions # +############################### +[*.cs] + +# 'using' directive placement +csharp_using_directive_placement = outside_namespace + +# var preferences +csharp_style_var_for_built_in_types = true:none +csharp_style_var_when_type_is_apparent = true:none +csharp_style_var_elsewhere = true:none + +# Expression-bodied members +csharp_style_expression_bodied_methods = false:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Pattern matching preferences +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion + +# Null-checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion + +# Expression-level preferences +csharp_prefer_braces = true:none +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +############################### +# C# Formatting Rules # +############################### +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false + +# Wrapping preferences +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true + +############################### +# Ignored diagnostics # +############################### +# CA2007: Consider calling ConfigureAwait on the awaited task +dotnet_diagnostic.CA2007.severity = none + +# CA1303: Do not pass literals as localized parameters +dotnet_diagnostic.CA1303.severity = none + +# CA1062: Validate arguments of public methods +dotnet_diagnostic.CA1062.severity = none + +# CA1305: Specify IFormatProvider +dotnet_diagnostic.CA1305.severity = none + +# CA2234: Pass system uri objects instead of strings +dotnet_diagnostic.CA2234.severity = none + +# CA1819: Properties should not return arrays +dotnet_diagnostic.CA1819.severity = none + +# CA2227: Collection properties should be read only +dotnet_diagnostic.CA2227.severity = none + +# CA1031: Do not catch general exception types +dotnet_diagnostic.CA1031.severity = none + +# CA1707: Remove the underscores from member names +dotnet_diagnostic.CA1707.severity = none + +# CS1591: XML Comments +dotnet_diagnostic.CS1591.severity = none; diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..bbfb89a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +--- +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + target-branch: dev + schedule: + interval: "daily" + reviewers: + - "ditkrg/devops" + + # Maintain dependencies for nuget + - package-ecosystem: "nuget" + directory: "/" + target-branch: dev + schedule: + interval: "daily" + reviewers: + - "ditkrg/digital-development-net" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..8e4ef79 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,36 @@ +name: "CodeQL" + +on: + push: + branches: [main] + pull_request: + # The branches below must be a subset of the branches above + branches: [main] + schedule: + - cron: "45 21 * * 2" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: csharp + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/deploy-nuget.yaml b/.github/workflows/deploy-nuget.yaml new file mode 100644 index 0000000..a1d511b --- /dev/null +++ b/.github/workflows/deploy-nuget.yaml @@ -0,0 +1,44 @@ +--- +name: Deploy To Nuget + +on: + push: + tags: + - "[0-9]+.[0-9]+.[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+-[a-z]+[0-9a-z]+" + + paths-ignore: + - "**.md" + - ".vscode/**" + +concurrency: + group: deploy-${{ github.ref_name }} + cancel-in-progress: true + +jobs: + test: + uses: ./.github/workflows/tests-base.yaml + + build-push: + name: Build and Publish + needs: [test] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-dotnet@v3 + with: + dotnet-version: "7.0.x" + + - name: Restore packages + run: dotnet restore + + - name: Create Pack + run: dotnet pack --no-restore --property:PackageOutputPath= ${{ runner.temp }}/packages --property:PackageVersion=${{ github.ref_name }} + + - name: Publish to Nuget + run: |- + dotnet nuget push "${{ runner.temp }}/packages/*.nupkg" \ + --skip-duplicate \ + --api-key ${{ secrets.NUGET_API_KEY }} \ + --source https://api.nuget.org/v3/index.json diff --git a/.github/workflows/sonarqube.yaml b/.github/workflows/sonarqube.yaml new file mode 100644 index 0000000..2508672 --- /dev/null +++ b/.github/workflows/sonarqube.yaml @@ -0,0 +1,51 @@ +name: Run SonarQube Analysis + +on: + push: + branches: + - main + + paths-ignore: + - "**.md" + - ".vscode/**" + +concurrency: + group: sonarqube-analysis + +jobs: + run-tests: + name: Run SonarQube Analysis + timeout-minutes: 10 + runs-on: ubuntu-latest + + env: + PROJECT_KEY: ditkrg_DIT.Workflower_AYF14rjSb80e2b0bns3t + SONARQUBE_HOST: ${{ secrets.SONARQUBE_HOST }} + SONARQUBE_TOKEN: ${{ secrets.SONARQUBE_TOKEN }} + ASPNETCORE_ENVIRONMENT: Testing + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + distribution: "zulu" + java-version: "11" + + - name: Restore tools + run: dotnet tool restore + + - name: Run Tests (SonarQube) + run: | + dotnet tool run dotnet-sonarscanner begin -k:"$PROJECT_KEY" \ + -d:sonar.login="$SONARQUBE_TOKEN" \ + -d:sonar.host.url="$SONARQUBE_HOST" \ + -d:sonar.cs.vscoveragexml.reportsPaths=coverage.xml + + dotnet build --no-incremental + dotnet dotnet-coverage collect "dotnet test" -f xml -o "coverage.xml" + + dotnet tool run dotnet-sonarscanner end -d:sonar.login="$SONARQUBE_TOKEN" diff --git a/.github/workflows/tests-base.yaml b/.github/workflows/tests-base.yaml new file mode 100644 index 0000000..cfd3a84 --- /dev/null +++ b/.github/workflows/tests-base.yaml @@ -0,0 +1,33 @@ +name: Run Tests + +on: + push: + branches-ignore: + - main + + paths-ignore: + - "**.md" + + - ".github/**" + - "!.github/workflows/tests-base.yaml" + + workflow_call: + +jobs: + run-tests: + name: Run Tests + timeout-minutes: 10 + runs-on: ubuntu-latest + + env: + ASPNETCORE_ENVIRONMENT: Testing + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-dotnet@v2 + with: + dotnet-version: "6.0.x" + + - name: Run tests + run: dotnet test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9965de2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,477 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk diff --git a/DIT.Authentication.sln b/DIT.Authentication.sln new file mode 100644 index 0000000..9a0ac2d --- /dev/null +++ b/DIT.Authentication.sln @@ -0,0 +1,27 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{59A96DA1-C499-4B80-AAA5-418EEE61BD96}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GatewayAuth", "src\GatewayAuth\GatewayAuth.csproj", "{EB268098-C712-49D9-9B1B-E6D09A4153F3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EB268098-C712-49D9-9B1B-E6D09A4153F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB268098-C712-49D9-9B1B-E6D09A4153F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB268098-C712-49D9-9B1B-E6D09A4153F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB268098-C712-49D9-9B1B-E6D09A4153F3}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {EB268098-C712-49D9-9B1B-E6D09A4153F3} = {59A96DA1-C499-4B80-AAA5-418EEE61BD96} + EndGlobalSection +EndGlobal diff --git a/src/GatewayAuth/Abstractions.cs b/src/GatewayAuth/Abstractions.cs new file mode 100644 index 0000000..d378ebb --- /dev/null +++ b/src/GatewayAuth/Abstractions.cs @@ -0,0 +1,21 @@ +using System.Security.Claims; + +namespace DIT.Authentication.GatewayAuth.Abstractions; + +public interface ISignatureValidator +{ + + void Initialize(GatewayAuthOptions options); + + Task ValidateSignatureAsync(string data, string signature); +} + +public interface IClaimsProvider +{ + Task GetClaimsAsync(string userHeader); +} + +public interface IUserInjector +{ + ValueTask SetUserAsync(UserModel user); +} diff --git a/src/GatewayAuth/Defaults.cs b/src/GatewayAuth/Defaults.cs new file mode 100644 index 0000000..8abdc71 --- /dev/null +++ b/src/GatewayAuth/Defaults.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; + +namespace DIT.Authentication.GatewayAuth; + +public static class GatewayAuthDefaults +{ + public const string AuthenticationScheme = "Gateway"; + + public const string ConfigurationSection = "Authentication:Gateway"; + + public const string UserHeader = "x-auth-user"; + + public const string SignatureHeader = "x-auth-signature"; +} diff --git a/src/GatewayAuth/EmptyHeaderHandler.cs b/src/GatewayAuth/EmptyHeaderHandler.cs new file mode 100644 index 0000000..6373a5e --- /dev/null +++ b/src/GatewayAuth/EmptyHeaderHandler.cs @@ -0,0 +1,32 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; + +namespace DIT.Authentication.GatewayAuth; + +public static class EmptyHeaderHandler +{ + public static readonly Func Error = ErrorHandler; + + public static readonly Func NoResult = NoResultHandler; + + public static readonly Func Success = SuccessHandler; + + private static AuthenticateResult NoResultHandler(HttpContext _) => AuthenticateResult.NoResult(); + + private static AuthenticateResult ErrorHandler(HttpContext _) => AuthenticateResult.Fail("No authentication header found"); + + private static AuthenticateResult SuccessHandler(HttpContext _) + { + var claims = new List + { + new Claim(ClaimTypes.Role, "anonymous") + }; + + var identity = new ClaimsIdentity(claims, GatewayAuthDefaults.AuthenticationScheme); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, GatewayAuthDefaults.AuthenticationScheme); + + return AuthenticateResult.Success(ticket); + } +} diff --git a/src/GatewayAuth/Extensions/Base64JsonClaimProvider.cs b/src/GatewayAuth/Extensions/Base64JsonClaimProvider.cs new file mode 100644 index 0000000..d13ff5b --- /dev/null +++ b/src/GatewayAuth/Extensions/Base64JsonClaimProvider.cs @@ -0,0 +1,33 @@ +using System.Security.Claims; +using System.Text.Json; +using DIT.Authentication.GatewayAuth.Abstractions; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace DIT.Authentication.GatewayAuth.Extensions; + +public static partial class GatewayAuthExtensions +{ + public static AuthenticationBuilder AddBase64JsonClaimsProvider(this AuthenticationBuilder builder, Func> claimsFactory) + { + builder.Services.TryAddScoped(sp => new Base64JsonClaimsProvider(new(JsonSerializerDefaults.Web), claimsFactory, userInjector: sp.GetService>())); + + return builder; + } + + public static AuthenticationBuilder AddBase64JsonClaimsProvider(this AuthenticationBuilder builder, JsonSerializerOptions jsonSerializerOptions, Func> claimsFactory) + { + builder.Services.TryAddScoped(sp => new Base64JsonClaimsProvider(jsonSerializerOptions, claimsFactory, userInjector: sp.GetService>())); + + return builder; + } + + public static AuthenticationBuilder AddBase64JsonClaimsProvider(this AuthenticationBuilder builder, Func jsonSerializerOptions, Func> claimsFactory) + { + builder.Services.TryAddScoped(sp => new Base64JsonClaimsProvider(jsonSerializerOptions(sp), claimsFactory, userInjector: sp.GetService>())); + + return builder; + } + +} diff --git a/src/GatewayAuth/Extensions/GatewayAuthExtensions.cs b/src/GatewayAuth/Extensions/GatewayAuthExtensions.cs new file mode 100644 index 0000000..edcdaa6 --- /dev/null +++ b/src/GatewayAuth/Extensions/GatewayAuthExtensions.cs @@ -0,0 +1,96 @@ +using DIT.Authentication.GatewayAuth.Abstractions; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace DIT.Authentication.GatewayAuth.Extensions; + +public static partial class GatewayAuthExtensions +{ + /// + /// Enables Gateway authentication using the default scheme . + /// + /// Gateway authentication performs authentication by extracting and validating a user and signature from the and request headers. + /// + /// + /// The . + /// A reference to after the operation has completed. + public static AuthenticationBuilder AddGateway(this AuthenticationBuilder builder) + => builder.AddGateway(configSectionPath: GatewayAuthDefaults.ConfigurationSection); + + /// + /// Enables Gateway authentication using the default scheme . + /// + /// Gateway authentication performs authentication by extracting and validating a user and signature from the and request headers. + /// + /// + /// The . + /// A reference to after the operation has completed. + public static AuthenticationBuilder AddGateway(this AuthenticationBuilder builder, Action configureOptions) + => builder.AddGateway(configSectionPath: GatewayAuthDefaults.ConfigurationSection, configureOptions); + + /// + /// Enables Gateway authentication using a pre-defined scheme. + /// + /// Gateway authentication performs authentication by extracting and validating a user and signature from the and request headers. + /// + /// + /// The . + /// The section path in the configuration. + /// A reference to after the operation has completed. + public static AuthenticationBuilder AddGateway(this AuthenticationBuilder builder, string configSectionPath) + => builder.AddGateway(configSectionPath, authenticationScheme: GatewayAuthDefaults.AuthenticationScheme); + + /// + /// Enables Gateway authentication using a pre-defined scheme. + /// + /// Gateway authentication performs authentication by extracting and validating a user and signature from the and request headers. + /// + /// + /// The . + /// The section path in the configuration. + /// A reference to after the operation has completed. + public static AuthenticationBuilder AddGateway(this AuthenticationBuilder builder, string configSectionPath, Action configureOptions) + => builder.AddGateway(configSectionPath, authenticationScheme: GatewayAuthDefaults.AuthenticationScheme, displayName: null, configureOptions: configureOptions); + + /// + /// Enables Gateway authentication using the specified scheme. + /// + /// Gateway authentication performs authentication by extracting and validating a user and signature from the and request headers. + /// + /// + /// The . + /// The section path in the configuration. + /// The authentication scheme. + /// A reference to after the operation has completed. + public static AuthenticationBuilder AddGateway(this AuthenticationBuilder builder, string configSectionPath, string authenticationScheme) + => builder.AddGateway(configSectionPath, authenticationScheme, displayName: null, configureOptions: null); + + /// + /// Enables Gateway authentication using the specified scheme. + /// + /// Gateway authentication performs authentication by extracting and validating a user and signature from the and request headers. + /// + /// + /// The . + /// The authentication scheme. + /// The display name for the authentication handler. + /// A delegate that allows configuring . + /// A reference to after the operation has completed. + public static AuthenticationBuilder AddGateway(this AuthenticationBuilder builder, string configSectionPath, string authenticationScheme, string? displayName, Action? configureOptions) + { + builder.Services + .AddOptions() + .BindConfiguration(configSectionPath) + .ValidateDataAnnotations() + .ValidateOnStart() + ; + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, PostConfigureOptions>()); + builder.Services.TryAddSingleton(); + + return builder.AddScheme(authenticationScheme, displayName, configureOptions); + } + +} diff --git a/src/GatewayAuth/GatewayAuth.csproj b/src/GatewayAuth/GatewayAuth.csproj new file mode 100644 index 0000000..23ba831 --- /dev/null +++ b/src/GatewayAuth/GatewayAuth.csproj @@ -0,0 +1,19 @@ + + + + enable + net6.0;net7.0 + enable + DIT.Authentication.GatewayAuth + + + true + DIT.Authentication.GatewayAuth + Authentication;Gateway;DIT + + + + + + + diff --git a/src/GatewayAuth/GatewayAuthException.cs b/src/GatewayAuth/GatewayAuthException.cs new file mode 100644 index 0000000..26ce8a0 --- /dev/null +++ b/src/GatewayAuth/GatewayAuthException.cs @@ -0,0 +1,21 @@ +namespace DIT.Authentication.GatewayAuth; + +public enum GatewayAuthErrorCode +{ + header_missing, + header_invalid, + invalid_signature, + invalid_user_model, +} + +public sealed class GatewayAuthException : Exception +{ + + public GatewayAuthErrorCode Error { get; } + + public GatewayAuthException(GatewayAuthErrorCode error, string message) : base(message) + { + Error = error; + } + +} diff --git a/src/GatewayAuth/Handler.cs b/src/GatewayAuth/Handler.cs new file mode 100644 index 0000000..4c261a0 --- /dev/null +++ b/src/GatewayAuth/Handler.cs @@ -0,0 +1,84 @@ + +using System.Diagnostics.CodeAnalysis; +using System.Security.Claims; +using System.Text.Encodings.Web; +using DIT.Authentication.GatewayAuth.Abstractions; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace DIT.Authentication.GatewayAuth; + +public class GatewayAuthHandler : AuthenticationHandler +{ + + private readonly IClaimsProvider _claimsProvider; + private readonly ISignatureValidator _signatureValidator; + + public GatewayAuthHandler( + IClaimsProvider claimsProvider, + UrlEncoder encoder, + IOptionsMonitor options, + ILoggerFactory logger, + ISignatureValidator signatureValidator, + ISystemClock clock) : base(options, logger, encoder, clock) + { + _claimsProvider = claimsProvider; + _signatureValidator = signatureValidator; + } + + protected override async Task HandleAuthenticateAsync() + { + var userHeader = Request.Headers[Options.UserHeader].FirstOrDefault(); + var signatureHeader = Request.Headers[Options.SignatureHeader].FirstOrDefault(); + + if (string.IsNullOrEmpty(userHeader) && string.IsNullOrEmpty(signatureHeader)) + return Options.EmptyHeadersHandler?.Invoke(Context) ?? EmptyHeaderHandler.NoResult(Context); + + if (string.IsNullOrEmpty(userHeader)) + return AuthenticateResult.Fail(new GatewayAuthException(GatewayAuthErrorCode.header_missing, "User header is missing")); + + if (string.IsNullOrEmpty(signatureHeader)) + return AuthenticateResult.Fail(new GatewayAuthException(GatewayAuthErrorCode.header_missing, "Signature header is missing")); + + if (!ExtractSignatureValue(signatureHeader, out string? extractedSignature)) + return AuthenticateResult.Fail(new GatewayAuthException(GatewayAuthErrorCode.header_invalid, "Signature header has an empty value")); + + if (!await _signatureValidator.ValidateSignatureAsync(userHeader, extractedSignature)) + return AuthenticateResult.Fail(new GatewayAuthException(GatewayAuthErrorCode.invalid_signature, "Signature is invalid")); + + try + { + var claimsIdentity = await _claimsProvider.GetClaimsAsync(userHeader); + var ticket = new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), Scheme.Name); + return AuthenticateResult.Success(ticket); + } + catch (GatewayAuthException e) + { + return AuthenticateResult.Fail(e); + } + } + + private static bool ExtractSignatureValue(string signatureHeader, [NotNullWhen(true)] out string? signature) + { + const string signaturePrefix = "signature="; + var span = signatureHeader.AsSpan(); + var i = span.IndexOf(signaturePrefix, StringComparison.OrdinalIgnoreCase); + + if (i < 0) + { + signature = null; + return false; + } + + span = span[(i + signaturePrefix.Length)..]; + var seperatorIndex = span.IndexOf(','); + + if (seperatorIndex >= 0) + span = span[..seperatorIndex]; + + signature = span.ToString(); + return true; + } + +} diff --git a/src/GatewayAuth/Implementations/Base64JsonClaimsProvider.cs b/src/GatewayAuth/Implementations/Base64JsonClaimsProvider.cs new file mode 100644 index 0000000..b88eb6d --- /dev/null +++ b/src/GatewayAuth/Implementations/Base64JsonClaimsProvider.cs @@ -0,0 +1,37 @@ +using System.Security.Claims; +using System.Text.Json; +using DIT.Authentication.GatewayAuth.Abstractions; + +namespace DIT.Authentication.GatewayAuth; + +public sealed class Base64JsonClaimsProvider : IClaimsProvider +{ + + private readonly JsonSerializerOptions _jsonSerializerOptions; + private readonly Func> _claimsFactory; + private readonly IUserInjector? _userInjector; + + public Base64JsonClaimsProvider(JsonSerializerOptions serializerOptions, Func> claimsFactory, IUserInjector? userInjector = null) + { + _userInjector = userInjector; + _claimsFactory = claimsFactory; + _jsonSerializerOptions = serializerOptions; + } + + public async Task GetClaimsAsync(string userHeader) + { + var decoded = Convert.FromBase64String(userHeader); + var resp = JsonSerializer.Deserialize(decoded, _jsonSerializerOptions); + + if (resp is null) + throw new GatewayAuthException(GatewayAuthErrorCode.invalid_user_model, "Unable to deserialize user model"); + + if (_userInjector is not null) + await _userInjector.SetUserAsync(resp); + + var claims = _claimsFactory.Invoke(resp); + var identity = new ClaimsIdentity(claims, nameof(GatewayAuthHandler)); + + return identity; + } +} diff --git a/src/GatewayAuth/Implementations/CertificateSignatureValidator.cs b/src/GatewayAuth/Implementations/CertificateSignatureValidator.cs new file mode 100644 index 0000000..a82a3de --- /dev/null +++ b/src/GatewayAuth/Implementations/CertificateSignatureValidator.cs @@ -0,0 +1,33 @@ +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using DIT.Authentication.GatewayAuth.Abstractions; + +namespace DIT.Authentication.GatewayAuth; + +internal sealed class CertificateSignatureValidator : ISignatureValidator +{ + private RSA _rsa = default!; + + public CertificateSignatureValidator() { } + + public void Initialize(GatewayAuthOptions options) + { + if (_rsa is not null) return; + + var certificate = new X509Certificate2(Encoding.ASCII.GetBytes(options.Certificate)); + _rsa = certificate.GetRSAPublicKey() ?? throw new InvalidOperationException("Could not get RSA public key from certificate"); + } + + public Task ValidateSignatureAsync(string data, string signature) + { + if (_rsa == null) throw new InvalidOperationException("RSA is null"); + + var dataBytes = Encoding.UTF8.GetBytes(data); + var signatureBytes = Convert.FromBase64String(signature); + + var isValid = _rsa.VerifyData(dataBytes, signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + + return Task.FromResult(isValid); + } +} diff --git a/src/GatewayAuth/Options/Options.cs b/src/GatewayAuth/Options/Options.cs new file mode 100644 index 0000000..11685a7 --- /dev/null +++ b/src/GatewayAuth/Options/Options.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; + +namespace DIT.Authentication.GatewayAuth; + +public class GatewayAuthOptions : AuthenticationSchemeOptions +{ + [Required] + public string Certificate { get; set; } = string.Empty; + + [Required] + public string UserHeader { get; set; } = GatewayAuthDefaults.UserHeader; + + [Required] + public string SignatureHeader { get; set; } = GatewayAuthDefaults.SignatureHeader; + + public Func? EmptyHeadersHandler { get; set; } +} diff --git a/src/GatewayAuth/Options/PostConfigureOptions.cs b/src/GatewayAuth/Options/PostConfigureOptions.cs new file mode 100644 index 0000000..5311371 --- /dev/null +++ b/src/GatewayAuth/Options/PostConfigureOptions.cs @@ -0,0 +1,22 @@ +using DIT.Authentication.GatewayAuth.Abstractions; +using Microsoft.Extensions.Options; + +namespace DIT.Authentication.GatewayAuth; + +public sealed class PostConfigureOptions : IPostConfigureOptions +{ + private readonly ISignatureValidator _signatureValidator; + + public PostConfigureOptions(ISignatureValidator signatureValidator) + { + _signatureValidator = signatureValidator; + } + + public void PostConfigure(string? name, GatewayAuthOptions options) + { + if (options.Certificate == null) + throw new InvalidOperationException("Certificate is null"); + + _signatureValidator.Initialize(options); + } +}