diff --git a/.github/workflows/Abstract.yml b/.github/workflows/Abstract.yml
deleted file mode 100644
index 77c8ded..0000000
--- a/.github/workflows/Abstract.yml
+++ /dev/null
@@ -1,52 +0,0 @@
-name: .NET
-
-on:
- push:
- branches: [ "main" ]
- pull_request:
- branches: [ "main" ]
-
-jobs:
- build:
-
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v4
- - name: Setup .NET
- uses: actions/setup-dotnet@v4
- with:
- dotnet-version: 8.0.x
- - name: Restore dependencies
- run: dotnet restore
- working-directory: ./MiddleWare/Autorizacion.Abstracciones
- - name: Build
- run: dotnet build --no-restore --configuration Release
- working-directory: ./MiddleWare/Autorizacion.Abstracciones
- - name: Pack
- run: dotnet pack --configuration Release
- working-directory: ./MiddleWare/Autorizacion.Abstracciones
- - uses: actions/upload-artifact@v4
- with:
- name: Autorizacion.Abstracciones
- if-no-files-found: error
- retention-days: 7
- path: ./MiddleWare/Autorizacion.Abstracciones/bin/Release/*.nupkg
-
- release:
- runs-on: ubuntu-latest
- needs: build
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Downlod Artifacts
- uses: actions/download-artifact@v4
-
- - name: List Files
- run: ls -R
-
- - name: Prep packages
- run: dotnet nuget add source --username Isma-L154 --password ${{ secrets.NUGET_TOKEN }} --store-password-in-clear-text --name Package "https://nuget.pkg.github.com/Isma-L154/index.json"
- - name: Push package to GitHub packages
- run: dotnet nuget push Autorizacion.Abstracciones/*.nupkg --api-key ${{ secrets.NUGET_TOKEN }} --source "Package" --skip-duplicate
diff --git a/.github/workflows/Auth.yml b/.github/workflows/Auth.yml
deleted file mode 100644
index 64caac9..0000000
--- a/.github/workflows/Auth.yml
+++ /dev/null
@@ -1,53 +0,0 @@
-name: .NET
-
-on:
- push:
- branches: [ "main" ]
- pull_request:
- branches: [ "main" ]
-
-jobs:
- build:
-
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v4
- - name: Setup .NET
- uses: actions/setup-dotnet@v4
- with:
- dotnet-version: 8.0.x
- - name: Restore dependencies
- run: dotnet restore
- working-directory: ./MiddleWare
- - name: Build
- run: dotnet build --no-restore --configuration Release
- working-directory: ./MiddleWare
- - name: Pack
- run: dotnet pack --configuration Release --no-build --output ./nupkgs
- working-directory: ./MiddleWare
-
- - uses: actions/upload-artifact@v4
- with:
- name: Autorizacion.Middleware
- if-no-files-found: error
- retention-days: 7
- path: /home/runner/work/MiddleWare/MiddleWare/MiddleWare/*/*/*/*.nupkg
-
- release:
- runs-on: ubuntu-latest
- needs: build
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Downlod Artifacts
- uses: actions/download-artifact@v4
-
- - name: List Files
- run: ls -R
-
- - name: Prep packages
- run: dotnet nuget add source --username Isma-L154 --password ${{ secrets.NUGET_TOKEN }} --store-password-in-clear-text --name Package "https://nuget.pkg.github.com/Isma-L154/index.json"
- - name: Push package to GitHub packages
- run: dotnet nuget push Autorizacion.Middleware/*/*/*/*.nupkg --api-key ${{ secrets.NUGET_TOKEN }} --source "Package" --skip-duplicate
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..9f320cc
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,42 @@
+name: CI
+
+on:
+ push:
+ branches-ignore: [] # build every branch push
+ pull_request:
+ branches: [ "main" ]
+ workflow_dispatch:
+
+# Cancel superseded runs on the same ref to save minutes.
+concurrency:
+ group: ci-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build-and-test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+
+ - name: Cache NuGet packages
+ uses: actions/cache@v4
+ with:
+ path: ~/.nuget/packages
+ key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj', '**/Directory.Build.props') }}
+ restore-keys: nuget-${{ runner.os }}-
+
+ - name: Restore
+ run: dotnet restore Authorization.sln
+
+ - name: Build
+ run: dotnet build Authorization.sln --no-restore --configuration Release
+
+ - name: Test
+ run: dotnet test Authorization.sln --no-build --configuration Release --verbosity normal
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..e372b78
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,56 @@
+name: Release
+
+on:
+ push:
+ branches: [ "main" ]
+ workflow_dispatch:
+
+# Use the built-in GITHUB_TOKEN to publish to GitHub Packages —
+# no personal access token to manage or rotate.
+permissions:
+ contents: read
+ packages: write
+
+concurrency:
+ group: release-main
+ cancel-in-progress: false
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+
+ - name: Restore
+ run: dotnet restore Authorization.sln
+
+ - name: Build
+ run: dotnet build Authorization.sln --no-restore --configuration Release
+
+ - name: Test
+ run: dotnet test Authorization.sln --no-build --configuration Release
+
+ - name: Pack
+ run: dotnet pack Authorization.sln --no-build --configuration Release --output "${{ github.workspace }}/artifacts"
+
+ - name: Upload packages artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: nuget-packages
+ if-no-files-found: error
+ retention-days: 7
+ path: ${{ github.workspace }}/artifacts/*.nupkg
+
+ - name: Push to GitHub Packages
+ run: >
+ dotnet nuget push "${{ github.workspace }}/artifacts/*.nupkg"
+ --api-key ${{ secrets.GITHUB_TOKEN }}
+ --source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json"
+ --skip-duplicate
diff --git a/.gitignore b/.gitignore
index a4fe18b..01e3c21 100644
--- a/.gitignore
+++ b/.gitignore
@@ -398,3 +398,10 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
+
+# Claude / AI assistant local files (never commit)
+CLAUDE.md
+.claude/
+.claude*/
+CLAUDE.local.md
+**/CLAUDE.md
diff --git a/Authorization.sln b/Authorization.sln
new file mode 100644
index 0000000..4e844bf
--- /dev/null
+++ b/Authorization.sln
@@ -0,0 +1,114 @@
+
+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", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Authorization.Abstractions", "src\Authorization.Abstractions\Authorization.Abstractions.csproj", "{60BA81F8-B279-47E3-8786-6FAE86C81FD3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Authorization.Common", "src\Authorization.Common\Authorization.Common.csproj", "{E8AF7B80-0A65-45E5-8F73-357A0FFFF699}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Authorization.DataAccess", "src\Authorization.DataAccess\Authorization.DataAccess.csproj", "{03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Authorization.Business", "src\Authorization.Business\Authorization.Business.csproj", "{8B921C4E-CCBD-49DE-91F8-0955695BC2D7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Authorization.Middleware", "src\Authorization.Middleware\Authorization.Middleware.csproj", "{C3F978CB-98CB-46B9-82FB-D795B1D76DA4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Authorization.UnitTests", "tests\Authorization.UnitTests\Authorization.UnitTests.csproj", "{E3F53F4A-E25E-4221-877B-81917F7C6C5A}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Debug|x64.Build.0 = Debug|Any CPU
+ {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Debug|x86.Build.0 = Debug|Any CPU
+ {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Release|x64.ActiveCfg = Release|Any CPU
+ {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Release|x64.Build.0 = Release|Any CPU
+ {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Release|x86.ActiveCfg = Release|Any CPU
+ {60BA81F8-B279-47E3-8786-6FAE86C81FD3}.Release|x86.Build.0 = Release|Any CPU
+ {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Debug|x64.Build.0 = Debug|Any CPU
+ {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Debug|x86.Build.0 = Debug|Any CPU
+ {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Release|x64.ActiveCfg = Release|Any CPU
+ {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Release|x64.Build.0 = Release|Any CPU
+ {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Release|x86.ActiveCfg = Release|Any CPU
+ {E8AF7B80-0A65-45E5-8F73-357A0FFFF699}.Release|x86.Build.0 = Release|Any CPU
+ {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Debug|x64.Build.0 = Debug|Any CPU
+ {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Debug|x86.Build.0 = Debug|Any CPU
+ {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Release|x64.ActiveCfg = Release|Any CPU
+ {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Release|x64.Build.0 = Release|Any CPU
+ {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Release|x86.ActiveCfg = Release|Any CPU
+ {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0}.Release|x86.Build.0 = Release|Any CPU
+ {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Debug|x64.Build.0 = Debug|Any CPU
+ {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Debug|x86.Build.0 = Debug|Any CPU
+ {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Release|x64.ActiveCfg = Release|Any CPU
+ {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Release|x64.Build.0 = Release|Any CPU
+ {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Release|x86.ActiveCfg = Release|Any CPU
+ {8B921C4E-CCBD-49DE-91F8-0955695BC2D7}.Release|x86.Build.0 = Release|Any CPU
+ {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Debug|x64.Build.0 = Debug|Any CPU
+ {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Debug|x86.Build.0 = Debug|Any CPU
+ {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Release|x64.ActiveCfg = Release|Any CPU
+ {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Release|x64.Build.0 = Release|Any CPU
+ {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Release|x86.ActiveCfg = Release|Any CPU
+ {C3F978CB-98CB-46B9-82FB-D795B1D76DA4}.Release|x86.Build.0 = Release|Any CPU
+ {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Debug|x64.Build.0 = Debug|Any CPU
+ {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Debug|x86.Build.0 = Debug|Any CPU
+ {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Release|x64.ActiveCfg = Release|Any CPU
+ {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Release|x64.Build.0 = Release|Any CPU
+ {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Release|x86.ActiveCfg = Release|Any CPU
+ {E3F53F4A-E25E-4221-877B-81917F7C6C5A}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {60BA81F8-B279-47E3-8786-6FAE86C81FD3} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+ {E8AF7B80-0A65-45E5-8F73-357A0FFFF699} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+ {03B4B2E4-7A31-44B1-87B4-3A939DAD29E0} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+ {8B921C4E-CCBD-49DE-91F8-0955695BC2D7} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+ {C3F978CB-98CB-46B9-82FB-D795B1D76DA4} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+ {E3F53F4A-E25E-4221-877B-81917F7C6C5A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+ EndGlobalSection
+EndGlobal
diff --git a/MiddleWare/Autorizacion.Abstracciones/Autorizacion.Abstracciones.csproj b/MiddleWare/Autorizacion.Abstracciones/Autorizacion.Abstracciones.csproj
deleted file mode 100644
index 2b3967c..0000000
--- a/MiddleWare/Autorizacion.Abstracciones/Autorizacion.Abstracciones.csproj
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
- net8.0
- enable
- enable
- 1.2.0
- True
-
-
-
-
-
-
diff --git a/MiddleWare/Autorizacion.Abstracciones/BW/IAutorizacionBW.cs b/MiddleWare/Autorizacion.Abstracciones/BW/IAutorizacionBW.cs
deleted file mode 100644
index 4e4e1de..0000000
--- a/MiddleWare/Autorizacion.Abstracciones/BW/IAutorizacionBW.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using Autorizacion.Abstracciones.Modelos;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Autorizacion.Abstracciones.BW
-{
- public interface IAutorizacionBW
- {
- Task ObtenerUsuario(Usuario usuario);
- Task> ObtenerPerfilesxUsuario(Usuario usuario);
- }
-}
diff --git a/MiddleWare/Autorizacion.Abstracciones/DA/IRepositorioDapper.cs b/MiddleWare/Autorizacion.Abstracciones/DA/IRepositorioDapper.cs
deleted file mode 100644
index f222673..0000000
--- a/MiddleWare/Autorizacion.Abstracciones/DA/IRepositorioDapper.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Data.SqlClient;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Autorizacion.Abstracciones.DA
-{
- public interface IRepositorioDapper
- {
- SqlConnection ObtenerRepositorioDapper();
- }
-}
diff --git a/MiddleWare/Autorizacion.Abstracciones/DA/ISeguridadDA.cs b/MiddleWare/Autorizacion.Abstracciones/DA/ISeguridadDA.cs
deleted file mode 100644
index cc539a0..0000000
--- a/MiddleWare/Autorizacion.Abstracciones/DA/ISeguridadDA.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using Autorizacion.Abstracciones.Modelos;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Autorizacion.Abstracciones.DA
-{
- public interface ISeguridadDA
- {
- Task ObtenerUsuario(Usuario usuario);
- Task> ObtenerPerfilesxUsuario(Usuario usuario);
-
- }
-}
diff --git a/MiddleWare/Autorizacion.Abstracciones/Entidades/Perfil.cs b/MiddleWare/Autorizacion.Abstracciones/Entidades/Perfil.cs
deleted file mode 100644
index 0c43090..0000000
--- a/MiddleWare/Autorizacion.Abstracciones/Entidades/Perfil.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Autorizacion.Abstracciones.Entidades
-{
- public class Perfil
- {
- public int Id { get; set; }
- public string Nombre { get; set; }
- }
-}
diff --git a/MiddleWare/Autorizacion.Abstracciones/Entidades/Usuario.cs b/MiddleWare/Autorizacion.Abstracciones/Entidades/Usuario.cs
deleted file mode 100644
index e4e5a17..0000000
--- a/MiddleWare/Autorizacion.Abstracciones/Entidades/Usuario.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Autorizacion.Abstracciones.Entidades
-{
- public class Usuario
- {
- public Guid Id { get; set; }
- public string NombreUsuario { get; set; }
- public string PasswordHash { get; set; }
- public string CorreoElectronico { get; set; }
- }
-}
diff --git a/MiddleWare/Autorizacion.Abstracciones/Modelos/Perfil.cs b/MiddleWare/Autorizacion.Abstracciones/Modelos/Perfil.cs
deleted file mode 100644
index cd69c0f..0000000
--- a/MiddleWare/Autorizacion.Abstracciones/Modelos/Perfil.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Autorizacion.Abstracciones.Modelos
-{
- public class Perfil
- {
- public int Id { get; set; }
- public string Nombre { get; set; }
- }
-}
diff --git a/MiddleWare/Autorizacion.Abstracciones/Modelos/Usuario.cs b/MiddleWare/Autorizacion.Abstracciones/Modelos/Usuario.cs
deleted file mode 100644
index bf074df..0000000
--- a/MiddleWare/Autorizacion.Abstracciones/Modelos/Usuario.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Autorizacion.Abstracciones.Modelos
-{
- public class Usuario
- {
- public Guid Id { get; set; }
- public string NombreUsuario { get; set; }
- public string PasswordHash { get; set; }
- public string CorreoElectronico { get; set; }
- }
-}
diff --git a/MiddleWare/Autorizacion.Middleware/Autorizacion.Middleware.csproj b/MiddleWare/Autorizacion.Middleware/Autorizacion.Middleware.csproj
deleted file mode 100644
index b3304f0..0000000
--- a/MiddleWare/Autorizacion.Middleware/Autorizacion.Middleware.csproj
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
- net8.0
- enable
- enable
- 1.1.0
- True
-
-
-
-
-
-
-
-
-
-
diff --git a/MiddleWare/Autorizacion.Middleware/ClaimsPerfil.cs b/MiddleWare/Autorizacion.Middleware/ClaimsPerfil.cs
deleted file mode 100644
index d0976be..0000000
--- a/MiddleWare/Autorizacion.Middleware/ClaimsPerfil.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-using Autorizacion.Abstracciones.BW;
-using Autorizacion.Abstracciones.Modelos;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Configuration;
-using System.Security.Claims;
-
-namespace Autorizacion.Middleware
-{
- public class ClaimsPerfil
- {
- private readonly RequestDelegate _next;
- private readonly IConfiguration _configuration;
- private IAutorizacionBW _autorizacionBW;
-
- public ClaimsPerfil(RequestDelegate next, IConfiguration configuration)
- {
- _next = next;
- _configuration = configuration;
- }
- //Capturamos lo que hay e intercepta el flujo
- public async Task InvokeAsync(HttpContext httpContext, IAutorizacionBW autorizacionBW)
- {
- _autorizacionBW = autorizacionBW;
- ClaimsIdentity appIdentity = await ValidarAutorizacion(httpContext);
- httpContext.User.AddIdentity(appIdentity); //Se inyecta un Claim al Usuario
- await _next(httpContext); // Se lo mandamos a la aplicacion para que continue
- }
-
- private async Task ValidarAutorizacion(HttpContext httpContext)
- {
- var claims = new List();
- if (httpContext.User != null && httpContext.User.Identity.IsAuthenticated) //Si el usuario esta autenticado, obtenemos la info del usuario
- {
- await ObtenerUsuario(httpContext, claims);
- await ObtenerPerfiles(httpContext, claims);
- }
- var appIdentity = new ClaimsIdentity(claims); //Le mandamos la info previamente recolectada
- return appIdentity;
- }
-
- //Perfiles ----
- private async Task ObtenerPerfiles(HttpContext httpContext, List claims)
- {
- var perfiles = await obtenerInformacionPerfiles(httpContext);
- if (perfiles != null && perfiles.Any())
- {
- foreach (var perfil in perfiles)
- {
- //Realizamos un Claim del ID/Rol que tiene
- claims.Add(new Claim(ClaimTypes.Role, perfil.Id.ToString()));
- }
- }
- }
-
- private async Task> obtenerInformacionPerfiles(HttpContext httpContext)
- {
- //Obtenemos la info del perfil por medio del flujo del BW
- return await _autorizacionBW.ObtenerPerfilesxUsuario(new Abstracciones.Modelos.Usuario { NombreUsuario = httpContext.User.Claims.Where(c => c.Type == "usuario").FirstOrDefault().Value });
- }
-
- //Usuarios ----
- private async Task ObtenerUsuario(HttpContext httpContext, List claims)
- {
- var usuario = await obtenerInformacionUsuario(httpContext);
- if (usuario is not null && !string.IsNullOrEmpty(usuario.Id.ToString()) && !string.IsNullOrEmpty(usuario.NombreUsuario.ToString()) && !string.IsNullOrEmpty(usuario.CorreoElectronico.ToString()))
- {
- //Si todo es correcto, y ninguna info del user es null, añadimos un claim por cada atributo que sea necesario
- claims.Add(new Claim(ClaimTypes.Email, usuario.CorreoElectronico));
- claims.Add(new Claim(ClaimTypes.Name, usuario.NombreUsuario));
- claims.Add(new Claim("IdUsuario", usuario.Id.ToString()));
- }
- }
-
- private async Task obtenerInformacionUsuario(HttpContext httpContext)
- {
- //Obtenemos la info del usuario por medio del flujo del BW
- return await _autorizacionBW.ObtenerUsuario(new Abstracciones.Modelos.Usuario { NombreUsuario = httpContext.User.Claims.Where(c => c.Type == "usuario").FirstOrDefault().Value });
- }
- }
- public static class ClaimsUsuarioMiddlewareExtensions
- {
- public static IApplicationBuilder AutorizacionClaims(this IApplicationBuilder builder)
- {
- return builder.UseMiddleware();
- }
- }
-}
diff --git a/MiddleWare/BW/Autorizacion.BW.csproj b/MiddleWare/BW/Autorizacion.BW.csproj
deleted file mode 100644
index 32d7320..0000000
--- a/MiddleWare/BW/Autorizacion.BW.csproj
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
- net8.0
- enable
- enable
- 1.1.0
-
-
-
-
-
diff --git a/MiddleWare/BW/AutorizacionBW.cs b/MiddleWare/BW/AutorizacionBW.cs
deleted file mode 100644
index a171d8b..0000000
--- a/MiddleWare/BW/AutorizacionBW.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using Autorizacion.Abstracciones.BW;
-using Autorizacion.Abstracciones.DA;
-using Autorizacion.Abstracciones.Modelos;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Autorizacion.BW
-{
- public class AutorizacionBW : IAutorizacionBW
- {
- private ISeguridadDA _seguridadDA;
-
- public AutorizacionBW(ISeguridadDA seguridadDA)
- {
- _seguridadDA = seguridadDA;
- }
- public async Task> ObtenerPerfilesxUsuario(Usuario usuario)
- {
- return await _seguridadDA.ObtenerPerfilesxUsuario(usuario);
- }
-
- public async Task ObtenerUsuario(Usuario usuario)
- {
- return await _seguridadDA.ObtenerUsuario(usuario);
- }
- }
-}
diff --git a/MiddleWare/DA/Autorizacion.DA.csproj b/MiddleWare/DA/Autorizacion.DA.csproj
deleted file mode 100644
index e725811..0000000
--- a/MiddleWare/DA/Autorizacion.DA.csproj
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
- net8.0
- enable
- enable
- 1.3.0
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/MiddleWare/DA/Repos/RepositorioDapper.cs b/MiddleWare/DA/Repos/RepositorioDapper.cs
deleted file mode 100644
index 50cf6f1..0000000
--- a/MiddleWare/DA/Repos/RepositorioDapper.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using Autorizacion.Abstracciones.DA;
-using Microsoft.Extensions.Configuration;
-using System;
-using System.Collections.Generic;
-using System.Data.SqlClient;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Autorizacion.DA.Repos
-{
- public class RepositorioDapper : IRepositorioDapper
- {
- private readonly IConfiguration _configutarion;
- private SqlConnection _connection;
-
- public RepositorioDapper(IConfiguration configutarion)
- {
- _configutarion = configutarion;
- _connection = new SqlConnection(_configutarion.GetConnectionString("BDSeguridad"));
- }
-
- public SqlConnection ObtenerRepositorioDapper()
- {
- return _connection;
- }
- }
-}
diff --git a/MiddleWare/DA/SeguridadDA.cs b/MiddleWare/DA/SeguridadDA.cs
deleted file mode 100644
index 50bc1f8..0000000
--- a/MiddleWare/DA/SeguridadDA.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using Autorizacion.Abstracciones.DA;
-using Autorizacion.Abstracciones.Modelos;
-using Dapper;
-using Helpers;
-using System.Data.SqlClient;
-
-namespace Autorizacion.DA
-{
- public class SeguridadDA : ISeguridadDA
- {
- IRepositorioDapper _repositorioDapper;
- private SqlConnection _sqlConnection;
-
- public SeguridadDA(IRepositorioDapper repositorioDapper)
- {
- _repositorioDapper = repositorioDapper;
- _sqlConnection = _repositorioDapper.ObtenerRepositorioDapper();
- }
- public async Task> ObtenerPerfilesxUsuario(Usuario usuario)
- {
- string sql = @"[ObtenerPerfilesxUsuario]";
- var consulta = await _sqlConnection.QueryAsync(sql, new { CorreoElectronico = usuario.CorreoElectronico, NombreUsuario = usuario.NombreUsuario });
- return Convertidor.ConvertirLista(consulta);
- }
-
- public async Task ObtenerUsuario(Usuario usuario)
- {
- string sql = @"[ObtenerUsuario]";
- var consulta = await _sqlConnection.QueryAsync(sql, new { CorreoElectronico = usuario.CorreoElectronico, NombreUsuario = usuario.NombreUsuario });
- return Convertidor.Convertir(consulta.FirstOrDefault());
- }
- }
-}
diff --git a/MiddleWare/Helpers/Convertidor.cs b/MiddleWare/Helpers/Convertidor.cs
deleted file mode 100644
index d028925..0000000
--- a/MiddleWare/Helpers/Convertidor.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-namespace Helpers
-{
- public static class Convertidor
- {
- public static TModeloEntrada Clonar(TModeloEntrada elemento) where TModeloEntrada : class, new() => Convertidor.Convertir(elemento);
-
- public static TModeloSalida Convertir(
- TModeloEntrada elementoBase,
- Action reglaTransformacion = null)
- where TModeloEntrada : class
- where TModeloSalida : new()
- {
- return Mapeador.MapearObjetos(elementoBase, reglaTransformacion);
- }
-
- public static IEnumerable ConvertirLista(
- IEnumerable elementos,
- Action reglaTransformacion = null)
- where TModeloEntrada : class
- where TModeloSalida : new()
- {
- return (IEnumerable)elementos.Select((Func)(elementoBase => Mapeador.MapearObjetos(elementoBase, reglaTransformacion))).ToList();
- }
- }
-}
diff --git a/MiddleWare/Helpers/Helpers.csproj b/MiddleWare/Helpers/Helpers.csproj
deleted file mode 100644
index 6928042..0000000
--- a/MiddleWare/Helpers/Helpers.csproj
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
- net8.0
- enable
- enable
- 1.1.0
-
-
-
-
-
diff --git a/MiddleWare/Helpers/Mapeador.cs b/MiddleWare/Helpers/Mapeador.cs
deleted file mode 100644
index c9413fa..0000000
--- a/MiddleWare/Helpers/Mapeador.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Helpers
-{
- public static class Mapeador
- {
- public static TModeloSalida MapearObjetos(
- TModeloEntrada objOrigen,
- Action ReglaTransformacion = null)
- where TModeloEntrada : class
- where TModeloSalida : new()
- {
- Type type1 = typeof(TModeloEntrada);
- Type type2 = typeof(TModeloSalida);
- TModeloSalida destino = default(TModeloSalida);
- if ((object)objOrigen != null)
- {
- destino = (TModeloSalida)Activator.CreateInstance(type2);
- PropertyInfo[] properties1 = type1.GetProperties();
- PropertyInfo[] properties2 = type2.GetProperties();
- Mapeador.SincronizarObjetos(objOrigen, destino, properties1, properties2);
- if (ReglaTransformacion != null)
- ReglaTransformacion(objOrigen, destino);
- }
- return destino;
- }
-
- private static void SincronizarObjetos(
- TModeloEntrada origen,
- TModeloSalida destino,
- PropertyInfo[] propiedadesOrigen,
- PropertyInfo[] propiedadesDestino)
- {
- foreach (PropertyInfo propertyInfo1 in propiedadesOrigen)
- {
- string nombrePropiedad = propertyInfo1.Name;
- PropertyInfo propertyInfo2 = ((IEnumerable)propiedadesDestino).FirstOrDefault((Func)(x => x.Name == nombrePropiedad));
- if (propertyInfo2 != (PropertyInfo)null && propertyInfo2.CanWrite && propertyInfo2.GetIndexParameters().Length == 0 && propertyInfo1.PropertyType.Name == propertyInfo2.PropertyType.Name)
- {
- if ((propertyInfo2.PropertyType.IsClass ? 1 : (propertyInfo2.PropertyType.IsInterface ? 1 : 0)) == 0 | propertyInfo2.PropertyType.Name.Equals("String") | propertyInfo2.PropertyType.Name.EndsWith("[]"))
- {
- object obj = propertyInfo1.GetValue((object)origen, (object[])null);
- propertyInfo2.SetValue((object)destino, obj, (object[])null);
- }
- }
- }
- }
- }
-}
diff --git a/MiddleWare/MiddleWare.sln b/MiddleWare/MiddleWare.sln
deleted file mode 100644
index 6fbc6be..0000000
--- a/MiddleWare/MiddleWare.sln
+++ /dev/null
@@ -1,49 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.8.34525.116
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Autorizacion.Abstracciones", "Autorizacion.Abstracciones\Autorizacion.Abstracciones.csproj", "{F84E1C30-E36A-4809-989C-3BD1268EFD93}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Helpers", "Helpers\Helpers.csproj", "{270F74FB-FB5B-4472-9FC1-A554BBA780BD}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Autorizacion.DA", "DA\Autorizacion.DA.csproj", "{FDA5784D-8BC8-448E-A04E-D4E1C7716240}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Autorizacion.BW", "BW\Autorizacion.BW.csproj", "{A624FB44-349D-4E11-AA7F-B3EB69161494}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Autorizacion.Middleware", "Autorizacion.Middleware\Autorizacion.Middleware.csproj", "{1F0C08C4-F365-452E-A180-947C28C18DEA}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {F84E1C30-E36A-4809-989C-3BD1268EFD93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F84E1C30-E36A-4809-989C-3BD1268EFD93}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F84E1C30-E36A-4809-989C-3BD1268EFD93}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F84E1C30-E36A-4809-989C-3BD1268EFD93}.Release|Any CPU.Build.0 = Release|Any CPU
- {270F74FB-FB5B-4472-9FC1-A554BBA780BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {270F74FB-FB5B-4472-9FC1-A554BBA780BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {270F74FB-FB5B-4472-9FC1-A554BBA780BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {270F74FB-FB5B-4472-9FC1-A554BBA780BD}.Release|Any CPU.Build.0 = Release|Any CPU
- {FDA5784D-8BC8-448E-A04E-D4E1C7716240}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FDA5784D-8BC8-448E-A04E-D4E1C7716240}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FDA5784D-8BC8-448E-A04E-D4E1C7716240}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FDA5784D-8BC8-448E-A04E-D4E1C7716240}.Release|Any CPU.Build.0 = Release|Any CPU
- {A624FB44-349D-4E11-AA7F-B3EB69161494}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A624FB44-349D-4E11-AA7F-B3EB69161494}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A624FB44-349D-4E11-AA7F-B3EB69161494}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A624FB44-349D-4E11-AA7F-B3EB69161494}.Release|Any CPU.Build.0 = Release|Any CPU
- {1F0C08C4-F365-452E-A180-947C28C18DEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {1F0C08C4-F365-452E-A180-947C28C18DEA}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {1F0C08C4-F365-452E-A180-947C28C18DEA}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {1F0C08C4-F365-452E-A180-947C28C18DEA}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {6FA99B5F-1890-48FB-9F37-068EFA98EDDE}
- EndGlobalSection
-EndGlobal
diff --git a/README.md b/README.md
index 0cc0e12..7b52be0 100644
--- a/README.md
+++ b/README.md
@@ -1,52 +1,139 @@
-# 🔐 JWT Authentication & Authorization Middleware
+# 🔐 JWT Authorization Claims Middleware
-This side project is a simple yet effective **middleware** component designed to handle **user authentication and authorization using JSON Web Tokens (JWT)**. It is designed to be easily integrated into other projects by deploying it as a **NuGet package**.
+[](https://github.com/Isma-L154/MiddleWare/actions/workflows/ci.yml)
+[](https://dotnet.microsoft.com/)
+[](LICENSE)
-Middleware plays a vital role in web applications by acting as a filter between the HTTP request and the core application logic. This middleware ensures that only authenticated and authorized users can access protected routes or resources.
+An ASP.NET Core middleware that **enriches an already-authenticated request principal** with identity claims (the user's id, name and email) and one **role claim per profile**, resolved from a SQL Server security store. It's shipped as a small set of **NuGet packages** so it can be dropped into any .NET 8 web app.
+
+> This is a personal side project. It sits between the HTTP request and your application logic, turning a bare authenticated token into a fully-populated `ClaimsPrincipal` your authorization policies can rely on.
+
+---
+
+## ✨ What it does
+
+Once a request has been authenticated (by JWT bearer auth, for example), the middleware:
+
+1. Reads the configured user-name claim from the incoming principal.
+2. Looks up the matching user in the security database (via a stored procedure).
+3. Adds `Email`, `Name` and `IdUsuario` claims.
+4. Looks up the user's profiles and adds a `Role` claim for each one.
+
+If anything goes wrong resolving that data (missing claim, unknown user, database outage), the request **degrades gracefully**: it continues unenriched instead of crashing the pipeline.
+
+---
+
+## 🧱 Architecture
+
+The solution is layered so each concern is isolated and independently testable:
+
+| Project | Responsibility |
+| --- | --- |
+| `Authorization.Abstractions` | Contracts: entities, models, options and interfaces. No external dependencies. |
+| `Authorization.Common` | Cached, reflection-based object mapper used to map entities → models. |
+| `Authorization.DataAccess` | Dapper + `Microsoft.Data.SqlClient` access to stored procedures. |
+| `Authorization.Business` | Thin business layer orchestrating identity resolution. |
+| `Authorization.Middleware` | The ASP.NET Core middleware plus DI and pipeline extensions. |
+
+```
+Request ─▶ Authentication ─▶ ClaimsEnrichmentMiddleware ─▶ your app
+ │
+ IAuthorizationManager (Business)
+ │
+ ISecurityRepository (DataAccess)
+ │
+ IDbConnectionFactory ─▶ SQL Server
+```
---
-## 🧩 What is Middleware?
+## 📦 Installation
-A **middleware** is a function or component that processes HTTP requests and/or responses within the application pipeline. It's typically used to:
+The packages are published to **GitHub Packages**. Add the feed and install the entry-point package (it pulls the rest in transitively):
-- Authenticate and authorize users
-- Log incoming and outgoing traffic
-- Handle errors or exceptions
-- Modify requests or responses
-- Manage headers, sessions, or CORS
+```bash
+dotnet nuget add source "https://nuget.pkg.github.com/Isma-L154/index.json" \
+ --name github --username --password
+
+dotnet add package Authorization.Middleware
+```
---
-## ✅ Purpose of This Middleware
+## 🚀 Usage
+
+**1. Register the services** (wires the connection factory, repository and business manager):
+
+```csharp
+using Authorization.Middleware;
+
+builder.Services.AddAuthorizationClaims();
+```
-The main purpose of this middleware is to provide **basic but secure access control** through JWT-based authentication. Specifically, it:
+**2. Add the middleware to the pipeline**, after authentication:
-- 🔐 **Authenticates**: Verifies the validity of a JWT sent in the request (usually in the Authorization header).
-- 🔓 **Authorizes**: Checks whether the user has the required role or permissions to access a specific route or resource.
+```csharp
+app.UseAuthentication();
+app.UseAuthorizationClaims(); // enrich the principal
+app.UseAuthorization();
+```
+
+**3. Configure the connection string** in `appsettings.json`:
+
+```json
+{
+ "ConnectionStrings": {
+ "SecurityDb": "Server=...;Database=...;Trusted_Connection=True;Encrypt=True;"
+ }
+}
+```
---
-## 🔧 How It Works
+## ⚙️ Configuration
+
+Everything that used to be hard-coded is now configurable through `ClaimsEnrichmentOptions`:
+
+```csharp
+builder.Services.AddAuthorizationClaims(options =>
+{
+ options.ConnectionStringName = "SecurityDb"; // ConnectionStrings key
+ options.UserNameClaimType = "usuario"; // inbound JWT claim to read
+ options.GetUserProcedure = "ObtenerUsuario"; // stored procedure names
+ options.GetProfilesProcedure = "ObtenerPerfilesxUsuario";
+});
+```
+
+| Option | Default | Description |
+| --- | --- | --- |
+| `ConnectionStringName` | `SecurityDb` | Key under `ConnectionStrings` for the security DB. |
+| `UserNameClaimType` | `usuario` | Inbound claim type carrying the user name. |
+| `GetUserProcedure` | `ObtenerUsuario` | Stored procedure returning a user by name/email. |
+| `GetProfilesProcedure` | `ObtenerPerfilesxUsuario` | Stored procedure returning a user's profiles. |
+
+---
+
+## 🧪 Building & testing
+
+```bash
+dotnet build Authorization.sln -c Release
+dotnet test Authorization.sln -c Release
+```
-1. The client sends a request with a JWT in the `Authorization` header (e.g., `Bearer `).
-2. The middleware:
- - Verifies the token's validity and signature.
- - Decodes the token to extract user data (e.g., ID, roles).
- - Checks if the user has permission to access the endpoint.
-3. If the token is invalid or permissions are insufficient, the request is rejected with the appropriate HTTP status (`401 Unauthorized` or `403 Forbidden`).
+CI runs on every push and pull request; packages are published from the `main` branch.
---
-## 🛠️ Technologies Used
+## 🛠️ Technologies
-- **JSON Web Tokens (JWT)** for token generation and validation
-- Written in **[C# , .NET 8.0]**
-- Lightweight, reusable, and easily integrated into any route-based app
-- Deployed as a **NuGet package** for easy integration into other projects
+- **.NET 8.0**, C# latest
+- **Dapper** for micro-ORM data access
+- **Microsoft.Data.SqlClient** (the maintained SQL Server driver)
+- **xUnit** + **Moq** for unit tests
+- Distributed as **NuGet packages** via GitHub Packages
---
-## 📦 Deployment via NuGet
+## 📄 License
-This middleware is packaged and deployed as a **NuGet package**, making it easy to integrate into any **C# .NET Core** project. To install the middleware package in your project:
+Released under the [MIT License](LICENSE).
diff --git a/src/Authorization.Abstractions/Authorization.Abstractions.csproj b/src/Authorization.Abstractions/Authorization.Abstractions.csproj
new file mode 100644
index 0000000..acdd200
--- /dev/null
+++ b/src/Authorization.Abstractions/Authorization.Abstractions.csproj
@@ -0,0 +1,8 @@
+
+
+
+ Authorization.Abstractions
+ Contracts (entities, models, options and interfaces) for the JWT authorization middleware. Has no external dependencies.
+
+
+
diff --git a/src/Authorization.Abstractions/Business/IAuthorizationManager.cs b/src/Authorization.Abstractions/Business/IAuthorizationManager.cs
new file mode 100644
index 0000000..1f6751a
--- /dev/null
+++ b/src/Authorization.Abstractions/Business/IAuthorizationManager.cs
@@ -0,0 +1,26 @@
+using Authorization.Abstractions.Models;
+
+namespace Authorization.Abstractions.Business;
+
+///
+/// Business-layer entry point for resolving the identity data used to enrich
+/// the request principal (the current user and the profiles they hold).
+///
+public interface IAuthorizationManager
+{
+ ///
+ /// Resolves the full user record for the supplied lookup criteria.
+ ///
+ /// Partial user carrying at least a lookup key (user name or email).
+ /// Token used to cancel the underlying I/O.
+ /// The resolved user, or null when no match exists.
+ Task GetUserAsync(User user, CancellationToken cancellationToken = default);
+
+ ///
+ /// Resolves every profile (role) granted to the supplied user.
+ ///
+ /// User whose profiles are requested.
+ /// Token used to cancel the underlying I/O.
+ /// The profiles for the user; an empty sequence when none exist.
+ Task> GetProfilesForUserAsync(User user, CancellationToken cancellationToken = default);
+}
diff --git a/src/Authorization.Abstractions/DataAccess/IDbConnectionFactory.cs b/src/Authorization.Abstractions/DataAccess/IDbConnectionFactory.cs
new file mode 100644
index 0000000..4594dff
--- /dev/null
+++ b/src/Authorization.Abstractions/DataAccess/IDbConnectionFactory.cs
@@ -0,0 +1,20 @@
+using System.Data.Common;
+
+namespace Authorization.Abstractions.DataAccess;
+
+///
+/// Creates database connections on demand.
+///
+///
+/// Every call returns a brand-new, unopened connection. This is deliberate:
+/// a single is not thread-safe, so sharing one
+/// instance across concurrent requests corrupts state. Handing out fresh
+/// connections lets the underlying ADO.NET connection pool do its job.
+/// The abstract return type keeps this contract
+/// free of any concrete database-provider dependency.
+///
+public interface IDbConnectionFactory
+{
+ /// Creates a new, unopened database connection.
+ DbConnection CreateConnection();
+}
diff --git a/src/Authorization.Abstractions/DataAccess/ISecurityRepository.cs b/src/Authorization.Abstractions/DataAccess/ISecurityRepository.cs
new file mode 100644
index 0000000..1801b3f
--- /dev/null
+++ b/src/Authorization.Abstractions/DataAccess/ISecurityRepository.cs
@@ -0,0 +1,15 @@
+using Authorization.Abstractions.Models;
+
+namespace Authorization.Abstractions.DataAccess;
+
+///
+/// Data-access contract for the security store that backs authorization.
+///
+public interface ISecurityRepository
+{
+ /// Reads a single user matching the supplied lookup criteria.
+ Task GetUserAsync(User user, CancellationToken cancellationToken = default);
+
+ /// Reads every profile (role) granted to the supplied user.
+ Task> GetProfilesForUserAsync(User user, CancellationToken cancellationToken = default);
+}
diff --git a/src/Authorization.Abstractions/Entities/Profile.cs b/src/Authorization.Abstractions/Entities/Profile.cs
new file mode 100644
index 0000000..b966ead
--- /dev/null
+++ b/src/Authorization.Abstractions/Entities/Profile.cs
@@ -0,0 +1,13 @@
+namespace Authorization.Abstractions.Entities;
+
+///
+/// Database-facing representation of a security profile (role) a user can hold.
+///
+public sealed class Profile
+{
+ /// Unique identifier of the profile.
+ public int Id { get; set; }
+
+ /// Display name of the profile.
+ public string? Name { get; set; }
+}
diff --git a/src/Authorization.Abstractions/Entities/User.cs b/src/Authorization.Abstractions/Entities/User.cs
new file mode 100644
index 0000000..d6ab7f9
--- /dev/null
+++ b/src/Authorization.Abstractions/Entities/User.cs
@@ -0,0 +1,19 @@
+namespace Authorization.Abstractions.Entities;
+
+///
+/// Database-facing representation of a user, as returned by the security data store.
+///
+public sealed class User
+{
+ /// Unique identifier of the user.
+ public Guid Id { get; set; }
+
+ /// Login name of the user.
+ public string? UserName { get; set; }
+
+ /// Hashed password. Never expose this outside the data layer.
+ public string? PasswordHash { get; set; }
+
+ /// Email address of the user.
+ public string? Email { get; set; }
+}
diff --git a/src/Authorization.Abstractions/Models/Profile.cs b/src/Authorization.Abstractions/Models/Profile.cs
new file mode 100644
index 0000000..3629a98
--- /dev/null
+++ b/src/Authorization.Abstractions/Models/Profile.cs
@@ -0,0 +1,13 @@
+namespace Authorization.Abstractions.Models;
+
+///
+/// Domain model of a security profile (role) held by a user.
+///
+public sealed class Profile
+{
+ /// Unique identifier of the profile.
+ public int Id { get; set; }
+
+ /// Display name of the profile.
+ public string? Name { get; set; }
+}
diff --git a/src/Authorization.Abstractions/Models/User.cs b/src/Authorization.Abstractions/Models/User.cs
new file mode 100644
index 0000000..4cb82ea
--- /dev/null
+++ b/src/Authorization.Abstractions/Models/User.cs
@@ -0,0 +1,21 @@
+namespace Authorization.Abstractions.Models;
+
+///
+/// Domain model of a user, used by the business and middleware layers.
+/// Kept separate from the database entity so persistence changes never leak
+/// into the pipeline contract.
+///
+public sealed class User
+{
+ /// Unique identifier of the user.
+ public Guid Id { get; set; }
+
+ /// Login name of the user.
+ public string? UserName { get; set; }
+
+ /// Hashed password. Never surfaced in claims, logs or responses.
+ public string? PasswordHash { get; set; }
+
+ /// Email address of the user.
+ public string? Email { get; set; }
+}
diff --git a/src/Authorization.Abstractions/Options/ClaimsEnrichmentOptions.cs b/src/Authorization.Abstractions/Options/ClaimsEnrichmentOptions.cs
new file mode 100644
index 0000000..a8f2515
--- /dev/null
+++ b/src/Authorization.Abstractions/Options/ClaimsEnrichmentOptions.cs
@@ -0,0 +1,34 @@
+namespace Authorization.Abstractions.Options;
+
+///
+/// Configuration for the claims-enrichment middleware and the security store.
+/// Everything that used to be hard-coded (the inbound claim to read the user
+/// name from, and the stored-procedure names) is configurable here so the
+/// package can be reused without recompiling.
+///
+public sealed class ClaimsEnrichmentOptions
+{
+ ///
+ /// Name of the connection string (in ConnectionStrings) pointing at
+ /// the security database. Defaults to SecurityDb.
+ ///
+ public string ConnectionStringName { get; set; } = "SecurityDb";
+
+ ///
+ /// The inbound JWT claim type that carries the user name used to look the
+ /// user up. Defaults to usuario to preserve the historical contract.
+ ///
+ public string UserNameClaimType { get; set; } = "usuario";
+
+ ///
+ /// Stored procedure that returns a single user by user name / email.
+ /// Defaults to ObtenerUsuario (the existing database object name).
+ ///
+ public string GetUserProcedure { get; set; } = "ObtenerUsuario";
+
+ ///
+ /// Stored procedure that returns the profiles for a user.
+ /// Defaults to ObtenerPerfilesxUsuario (the existing database object name).
+ ///
+ public string GetProfilesProcedure { get; set; } = "ObtenerPerfilesxUsuario";
+}
diff --git a/src/Authorization.Business/Authorization.Business.csproj b/src/Authorization.Business/Authorization.Business.csproj
new file mode 100644
index 0000000..90e68a7
--- /dev/null
+++ b/src/Authorization.Business/Authorization.Business.csproj
@@ -0,0 +1,12 @@
+
+
+
+ Authorization.Business
+ Business layer for the JWT authorization middleware; orchestrates identity resolution over the data-access layer.
+
+
+
+
+
+
+
diff --git a/src/Authorization.Business/AuthorizationManager.cs b/src/Authorization.Business/AuthorizationManager.cs
new file mode 100644
index 0000000..9036357
--- /dev/null
+++ b/src/Authorization.Business/AuthorizationManager.cs
@@ -0,0 +1,29 @@
+using Authorization.Abstractions.Business;
+using Authorization.Abstractions.DataAccess;
+using Authorization.Abstractions.Models;
+
+namespace Authorization.Business;
+
+///
+/// Default that delegates identity
+/// resolution to the security data-access layer. It is intentionally thin:
+/// its role is to keep the middleware decoupled from persistence so business
+/// rules can grow here without touching the pipeline.
+///
+public sealed class AuthorizationManager : IAuthorizationManager
+{
+ private readonly ISecurityRepository _securityRepository;
+
+ public AuthorizationManager(ISecurityRepository securityRepository)
+ {
+ _securityRepository = securityRepository ?? throw new ArgumentNullException(nameof(securityRepository));
+ }
+
+ ///
+ public Task GetUserAsync(User user, CancellationToken cancellationToken = default)
+ => _securityRepository.GetUserAsync(user, cancellationToken);
+
+ ///
+ public Task> GetProfilesForUserAsync(User user, CancellationToken cancellationToken = default)
+ => _securityRepository.GetProfilesForUserAsync(user, cancellationToken);
+}
diff --git a/src/Authorization.Common/Authorization.Common.csproj b/src/Authorization.Common/Authorization.Common.csproj
new file mode 100644
index 0000000..fc3d216
--- /dev/null
+++ b/src/Authorization.Common/Authorization.Common.csproj
@@ -0,0 +1,8 @@
+
+
+
+ Authorization.Common
+ Shared helpers for the JWT authorization middleware, including a cached reflection-based object mapper.
+
+
+
diff --git a/src/Authorization.Common/Converter.cs b/src/Authorization.Common/Converter.cs
new file mode 100644
index 0000000..2435761
--- /dev/null
+++ b/src/Authorization.Common/Converter.cs
@@ -0,0 +1,46 @@
+namespace Authorization.Common;
+
+///
+/// Convenience facade over for single objects and sequences.
+///
+public static class Converter
+{
+ /// Creates a shallow copy of .
+ public static TModel? Clone(TModel? source)
+ where TModel : class, new()
+ => Mapper.Map(source);
+
+ /// Converts a single object to .
+ public static TDestination? Convert(
+ TSource? source,
+ Action? transform = null)
+ where TSource : class
+ where TDestination : new()
+ => Mapper.Map(source, transform);
+
+ ///
+ /// Converts a sequence, skipping any elements that map to null.
+ /// Materialised to a list so the (already-executed) source query is only
+ /// enumerated once.
+ ///
+ public static IReadOnlyList ConvertList(
+ IEnumerable source,
+ Action? transform = null)
+ where TSource : class
+ where TDestination : new()
+ {
+ ArgumentNullException.ThrowIfNull(source);
+
+ var result = new List();
+ foreach (var element in source)
+ {
+ var mapped = Mapper.Map(element, transform);
+ if (mapped is not null)
+ {
+ result.Add(mapped);
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/src/Authorization.Common/Mapper.cs b/src/Authorization.Common/Mapper.cs
new file mode 100644
index 0000000..0784255
--- /dev/null
+++ b/src/Authorization.Common/Mapper.cs
@@ -0,0 +1,87 @@
+using System.Collections.Concurrent;
+using System.Reflection;
+
+namespace Authorization.Common;
+
+///
+/// Lightweight convention-based object mapper: copies matching public
+/// properties (by name and type) from a source object to a new destination
+/// instance.
+///
+///
+/// This runs on the request hot path, so the reflection work (discovering
+/// which source/destination properties line up) is computed once per
+/// type-pair and cached. Only value types, strings and arrays are copied,
+/// mirroring the original shallow-copy behaviour so nested reference graphs
+/// are never shared by accident.
+///
+public static class Mapper
+{
+ private static readonly ConcurrentDictionary<(Type Source, Type Destination), PropertyPair[]> PropertyMapCache = new();
+
+ private readonly record struct PropertyPair(PropertyInfo Source, PropertyInfo Destination);
+
+ ///
+ /// Maps onto a new instance of
+ /// .
+ ///
+ /// Object to read values from. May be null.
+ /// Optional hook to apply custom rules after the automatic copy.
+ /// The populated destination, or the type default when is null.
+ public static TDestination? Map(
+ TSource? source,
+ Action? transform = null)
+ where TSource : class
+ where TDestination : new()
+ {
+ if (source is null)
+ {
+ return default;
+ }
+
+ var destination = new TDestination();
+ foreach (var pair in GetPropertyMap(typeof(TSource), typeof(TDestination)))
+ {
+ pair.Destination.SetValue(destination, pair.Source.GetValue(source));
+ }
+
+ transform?.Invoke(source, destination);
+ return destination;
+ }
+
+ private static PropertyPair[] GetPropertyMap(Type source, Type destination) =>
+ PropertyMapCache.GetOrAdd((source, destination), static key => BuildPropertyMap(key.Source, key.Destination));
+
+ private static PropertyPair[] BuildPropertyMap(Type source, Type destination)
+ {
+ var destinationProperties = destination
+ .GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .ToDictionary(p => p.Name, StringComparer.Ordinal);
+
+ var pairs = new List();
+ foreach (var sourceProperty in source.GetProperties(BindingFlags.Public | BindingFlags.Instance))
+ {
+ if (!destinationProperties.TryGetValue(sourceProperty.Name, out var destinationProperty))
+ {
+ continue;
+ }
+
+ if (!destinationProperty.CanWrite ||
+ destinationProperty.GetIndexParameters().Length != 0 ||
+ destinationProperty.PropertyType != sourceProperty.PropertyType ||
+ !IsCopyable(destinationProperty.PropertyType))
+ {
+ continue;
+ }
+
+ pairs.Add(new PropertyPair(sourceProperty, destinationProperty));
+ }
+
+ return pairs.ToArray();
+ }
+
+ // Copy value types, strings and arrays only; skip complex reference types
+ // to avoid sharing mutable nested objects between source and destination.
+ private static bool IsCopyable(Type type) =>
+ !type.IsClass || type == typeof(string) || type.IsArray;
+}
diff --git a/src/Authorization.DataAccess/Authorization.DataAccess.csproj b/src/Authorization.DataAccess/Authorization.DataAccess.csproj
new file mode 100644
index 0000000..b3485c2
--- /dev/null
+++ b/src/Authorization.DataAccess/Authorization.DataAccess.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Authorization.DataAccess
+ Dapper-based data access for the JWT authorization middleware, backed by SQL Server stored procedures.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Authorization.DataAccess/SecurityRepository.cs b/src/Authorization.DataAccess/SecurityRepository.cs
new file mode 100644
index 0000000..a624d76
--- /dev/null
+++ b/src/Authorization.DataAccess/SecurityRepository.cs
@@ -0,0 +1,63 @@
+using System.Data;
+using Authorization.Abstractions.DataAccess;
+using Authorization.Abstractions.Options;
+using Authorization.Common;
+using Dapper;
+using Microsoft.Extensions.Options;
+using Entities = Authorization.Abstractions.Entities;
+using Models = Authorization.Abstractions.Models;
+
+namespace Authorization.DataAccess;
+
+///
+/// Reads users and their profiles from the security database through stored
+/// procedures, using Dapper.
+///
+public sealed class SecurityRepository : ISecurityRepository
+{
+ private readonly IDbConnectionFactory _connectionFactory;
+ private readonly ClaimsEnrichmentOptions _options;
+
+ public SecurityRepository(IDbConnectionFactory connectionFactory, IOptions options)
+ {
+ _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
+ _options = (options ?? throw new ArgumentNullException(nameof(options))).Value;
+ }
+
+ ///
+ public async Task GetUserAsync(Models.User user, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(user);
+
+ // A fresh connection per call: SqlConnection is not thread-safe and
+ // must not be shared across concurrent requests. `await using` guarantees
+ // it is returned to the pool even if the query throws.
+ await using var connection = _connectionFactory.CreateConnection();
+
+ var command = new CommandDefinition(
+ _options.GetUserProcedure,
+ new { user.Email, user.UserName },
+ commandType: CommandType.StoredProcedure,
+ cancellationToken: cancellationToken);
+
+ var entity = await connection.QueryFirstOrDefaultAsync(command);
+ return Converter.Convert(entity);
+ }
+
+ ///
+ public async Task> GetProfilesForUserAsync(Models.User user, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(user);
+
+ await using var connection = _connectionFactory.CreateConnection();
+
+ var command = new CommandDefinition(
+ _options.GetProfilesProcedure,
+ new { user.Email, user.UserName },
+ commandType: CommandType.StoredProcedure,
+ cancellationToken: cancellationToken);
+
+ var entities = await connection.QueryAsync(command);
+ return Converter.ConvertList(entities);
+ }
+}
diff --git a/src/Authorization.DataAccess/SqlConnectionFactory.cs b/src/Authorization.DataAccess/SqlConnectionFactory.cs
new file mode 100644
index 0000000..763c442
--- /dev/null
+++ b/src/Authorization.DataAccess/SqlConnectionFactory.cs
@@ -0,0 +1,36 @@
+using System.Data.Common;
+using Authorization.Abstractions.DataAccess;
+using Authorization.Abstractions.Options;
+using Microsoft.Data.SqlClient;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+
+namespace Authorization.DataAccess;
+
+///
+/// SQL Server implementation of .
+///
+///
+/// The connection string is read once and cached; each call returns a fresh
+/// so callers can safely open, use and dispose it
+/// without any cross-request sharing. ADO.NET pools the physical connections
+/// underneath, so this is both correct and cheap.
+///
+public sealed class SqlConnectionFactory : IDbConnectionFactory
+{
+ private readonly string _connectionString;
+
+ public SqlConnectionFactory(IConfiguration configuration, IOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(configuration);
+ ArgumentNullException.ThrowIfNull(options);
+
+ var name = options.Value.ConnectionStringName;
+ _connectionString = configuration.GetConnectionString(name)
+ ?? throw new InvalidOperationException(
+ $"Connection string '{name}' was not found. Configure it under \"ConnectionStrings\".");
+ }
+
+ ///
+ public DbConnection CreateConnection() => new SqlConnection(_connectionString);
+}
diff --git a/src/Authorization.Middleware/Authorization.Middleware.csproj b/src/Authorization.Middleware/Authorization.Middleware.csproj
new file mode 100644
index 0000000..e744ddd
--- /dev/null
+++ b/src/Authorization.Middleware/Authorization.Middleware.csproj
@@ -0,0 +1,23 @@
+
+
+
+ Authorization.Middleware
+ ASP.NET Core middleware that enriches the authenticated principal with user and profile (role) claims resolved from a security store.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Authorization.Middleware/AuthorizationMiddlewareExtensions.cs b/src/Authorization.Middleware/AuthorizationMiddlewareExtensions.cs
new file mode 100644
index 0000000..beede5e
--- /dev/null
+++ b/src/Authorization.Middleware/AuthorizationMiddlewareExtensions.cs
@@ -0,0 +1,19 @@
+using Microsoft.AspNetCore.Builder;
+
+namespace Authorization.Middleware;
+
+///
+/// Pipeline registration for .
+///
+public static class AuthorizationMiddlewareExtensions
+{
+ ///
+ /// Adds the claims-enrichment middleware to the request pipeline. Place it
+ /// after authentication so the principal is already established.
+ ///
+ public static IApplicationBuilder UseAuthorizationClaims(this IApplicationBuilder builder)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ return builder.UseMiddleware();
+ }
+}
diff --git a/src/Authorization.Middleware/ClaimsEnrichmentMiddleware.cs b/src/Authorization.Middleware/ClaimsEnrichmentMiddleware.cs
new file mode 100644
index 0000000..cb42eef
--- /dev/null
+++ b/src/Authorization.Middleware/ClaimsEnrichmentMiddleware.cs
@@ -0,0 +1,132 @@
+using System.Security.Claims;
+using Authorization.Abstractions.Business;
+using Authorization.Abstractions.Models;
+using Authorization.Abstractions.Options;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Authorization.Middleware;
+
+///
+/// Enriches the authenticated principal with claims resolved from the security
+/// store: the user's identifier, name and email, plus a role claim per profile.
+///
+///
+/// Middleware is instantiated once (singleton), so no per-request state is kept
+/// in fields — the scoped is received through
+/// instead. Any failure while resolving identity data
+/// is logged and swallowed so a transient store outage degrades gracefully
+/// (the request continues unenriched) rather than crashing the pipeline.
+///
+public sealed partial class ClaimsEnrichmentMiddleware
+{
+ private readonly RequestDelegate _next;
+ private readonly ILogger _logger;
+ private readonly ClaimsEnrichmentOptions _options;
+
+ public ClaimsEnrichmentMiddleware(
+ RequestDelegate next,
+ ILogger logger,
+ IOptions options)
+ {
+ _next = next ?? throw new ArgumentNullException(nameof(next));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _options = (options ?? throw new ArgumentNullException(nameof(options))).Value;
+ }
+
+ /// Intercepts the request, enriches the principal, then forwards it.
+ public async Task InvokeAsync(HttpContext context, IAuthorizationManager authorizationManager)
+ {
+ var identity = await BuildClaimsIdentityAsync(context, authorizationManager);
+ if (identity is not null)
+ {
+ context.User.AddIdentity(identity);
+ }
+
+ await _next(context);
+ }
+
+ private async Task BuildClaimsIdentityAsync(HttpContext context, IAuthorizationManager authorizationManager)
+ {
+ // Only enrich real, authenticated principals; anonymous traffic flows through untouched.
+ if (context.User.Identity is not { IsAuthenticated: true })
+ {
+ return null;
+ }
+
+ var userName = context.User.FindFirst(_options.UserNameClaimType)?.Value;
+ if (string.IsNullOrWhiteSpace(userName))
+ {
+ LogMissingUserNameClaim(_options.UserNameClaimType, context.TraceIdentifier);
+ return null;
+ }
+
+ try
+ {
+ var claims = new List();
+ var user = await authorizationManager.GetUserAsync(new User { UserName = userName }, context.RequestAborted);
+ if (user is null)
+ {
+ LogUserNotFound(context.TraceIdentifier);
+ return null;
+ }
+
+ AddUserClaims(claims, user);
+ await AddProfileClaimsAsync(claims, user, authorizationManager, context.RequestAborted);
+
+ return new ClaimsIdentity(claims);
+ }
+ catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested)
+ {
+ // Client went away; nothing to log at error level.
+ return null;
+ }
+ catch (Exception ex)
+ {
+ // Graceful degradation: never let an identity-store failure crash the request.
+ LogEnrichmentFailed(ex, context.TraceIdentifier);
+ return null;
+ }
+ }
+
+ private static void AddUserClaims(ICollection claims, User user)
+ {
+ if (!string.IsNullOrEmpty(user.Email))
+ {
+ claims.Add(new Claim(ClaimTypes.Email, user.Email));
+ }
+
+ if (!string.IsNullOrEmpty(user.UserName))
+ {
+ claims.Add(new Claim(ClaimTypes.Name, user.UserName));
+ }
+
+ claims.Add(new Claim("IdUsuario", user.Id.ToString()));
+ }
+
+ private async Task AddProfileClaimsAsync(
+ ICollection claims,
+ User user,
+ IAuthorizationManager authorizationManager,
+ CancellationToken cancellationToken)
+ {
+ var profiles = await authorizationManager.GetProfilesForUserAsync(user, cancellationToken);
+ foreach (var profile in profiles)
+ {
+ claims.Add(new Claim(ClaimTypes.Role, profile.Id.ToString()));
+ }
+ }
+
+ [LoggerMessage(Level = LogLevel.Debug,
+ Message = "Skipping claims enrichment: inbound principal has no '{ClaimType}' claim. TraceId={TraceId}")]
+ private partial void LogMissingUserNameClaim(string claimType, string traceId);
+
+ [LoggerMessage(Level = LogLevel.Debug,
+ Message = "Skipping claims enrichment: no user matched the inbound claim. TraceId={TraceId}")]
+ private partial void LogUserNotFound(string traceId);
+
+ [LoggerMessage(Level = LogLevel.Error,
+ Message = "Claims enrichment failed; continuing without enriched claims. TraceId={TraceId}")]
+ private partial void LogEnrichmentFailed(Exception exception, string traceId);
+}
diff --git a/src/Authorization.Middleware/ServiceCollectionExtensions.cs b/src/Authorization.Middleware/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..042c11e
--- /dev/null
+++ b/src/Authorization.Middleware/ServiceCollectionExtensions.cs
@@ -0,0 +1,42 @@
+using Authorization.Abstractions.Business;
+using Authorization.Abstractions.DataAccess;
+using Authorization.Abstractions.Options;
+using Authorization.Business;
+using Authorization.DataAccess;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Authorization.Middleware;
+
+///
+/// Dependency-injection registration for the authorization stack. A single
+/// call wires the connection factory, repository and business manager so
+/// consumers no longer have to assemble the graph by hand.
+///
+public static class ServiceCollectionExtensions
+{
+ ///
+ /// Registers everything the claims-enrichment middleware needs.
+ ///
+ /// The service collection.
+ /// Optional hook to override connection-string name, claim type or stored-procedure names.
+ public static IServiceCollection AddAuthorizationClaims(
+ this IServiceCollection services,
+ Action? configure = null)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+
+ var optionsBuilder = services.AddOptions();
+ if (configure is not null)
+ {
+ optionsBuilder.Configure(configure);
+ }
+
+ // The factory only caches an immutable connection string, so it is safe
+ // as a singleton; it still hands out a fresh connection per call.
+ services.AddSingleton();
+ services.AddScoped();
+ services.AddScoped();
+
+ return services;
+ }
+}
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
new file mode 100644
index 0000000..8d0c512
--- /dev/null
+++ b/src/Directory.Build.props
@@ -0,0 +1,41 @@
+
+
+
+
+ net8.0
+ latest
+ enable
+ enable
+ true
+ true
+
+ $(NoWarn);CS1591
+
+
+
+
+ 2.0.0
+ Isma-L154
+ Isma-L154
+ JWT Authorization Middleware
+ MIT
+ https://github.com/Isma-L154/MiddleWare
+ https://github.com/Isma-L154/MiddleWare
+ git
+ middleware;jwt;authentication;authorization;aspnetcore;claims
+ README.md
+ true
+ snupkg
+
+ false
+
+
+
+
+
+
+
diff --git a/tests/Authorization.UnitTests/Authorization.UnitTests.csproj b/tests/Authorization.UnitTests/Authorization.UnitTests.csproj
new file mode 100644
index 0000000..dc14bcf
--- /dev/null
+++ b/tests/Authorization.UnitTests/Authorization.UnitTests.csproj
@@ -0,0 +1,33 @@
+
+
+
+ net8.0
+ enable
+ enable
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Authorization.UnitTests/AuthorizationManagerTests.cs b/tests/Authorization.UnitTests/AuthorizationManagerTests.cs
new file mode 100644
index 0000000..dd7fd74
--- /dev/null
+++ b/tests/Authorization.UnitTests/AuthorizationManagerTests.cs
@@ -0,0 +1,48 @@
+using Authorization.Abstractions.DataAccess;
+using Authorization.Abstractions.Models;
+using Authorization.Business;
+using Moq;
+
+namespace Authorization.UnitTests;
+
+public class AuthorizationManagerTests
+{
+ [Fact]
+ public async Task GetUserAsync_DelegatesToRepository()
+ {
+ var expected = new User { Id = Guid.NewGuid(), UserName = "jdoe" };
+ var repository = new Mock();
+ repository
+ .Setup(r => r.GetUserAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(expected);
+
+ var manager = new AuthorizationManager(repository.Object);
+
+ var result = await manager.GetUserAsync(new User { UserName = "jdoe" });
+
+ Assert.Same(expected, result);
+ repository.Verify(r => r.GetUserAsync(It.IsAny(), It.IsAny()), Times.Once);
+ }
+
+ [Fact]
+ public async Task GetProfilesForUserAsync_DelegatesToRepository()
+ {
+ var expected = new[] { new Profile { Id = 1, Name = "admin" } };
+ var repository = new Mock();
+ repository
+ .Setup(r => r.GetProfilesForUserAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(expected);
+
+ var manager = new AuthorizationManager(repository.Object);
+
+ var result = await manager.GetProfilesForUserAsync(new User { UserName = "jdoe" });
+
+ Assert.Same(expected, result);
+ }
+
+ [Fact]
+ public void Constructor_NullRepository_Throws()
+ {
+ Assert.Throws(() => new AuthorizationManager(null!));
+ }
+}
diff --git a/tests/Authorization.UnitTests/ClaimsEnrichmentMiddlewareTests.cs b/tests/Authorization.UnitTests/ClaimsEnrichmentMiddlewareTests.cs
new file mode 100644
index 0000000..61e1afc
--- /dev/null
+++ b/tests/Authorization.UnitTests/ClaimsEnrichmentMiddlewareTests.cs
@@ -0,0 +1,122 @@
+using System.Security.Claims;
+using Authorization.Abstractions.Business;
+using Authorization.Abstractions.Models;
+using Authorization.Abstractions.Options;
+using Authorization.Middleware;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Moq;
+
+namespace Authorization.UnitTests;
+
+public class ClaimsEnrichmentMiddlewareTests
+{
+ private static readonly IOptions Options =
+ Microsoft.Extensions.Options.Options.Create(new ClaimsEnrichmentOptions());
+
+ [Fact]
+ public async Task Invoke_AnonymousUser_CallsNextWithoutEnrichment()
+ {
+ var manager = new Mock(MockBehavior.Strict);
+ var context = new DefaultHttpContext();
+ var nextCalled = false;
+
+ var middleware = new ClaimsEnrichmentMiddleware(_ => { nextCalled = true; return Task.CompletedTask; }, NullLogger.Instance, Options);
+
+ await middleware.InvokeAsync(context, manager.Object);
+
+ Assert.True(nextCalled);
+ Assert.DoesNotContain(context.User.Identities, i => i.HasClaim(c => c.Type == ClaimTypes.Name));
+ manager.VerifyNoOtherCalls();
+ }
+
+ [Fact]
+ public async Task Invoke_AuthenticatedUser_AddsUserAndRoleClaims()
+ {
+ var userId = Guid.NewGuid();
+ var manager = new Mock();
+ manager
+ .Setup(m => m.GetUserAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(new User { Id = userId, UserName = "jdoe", Email = "jdoe@example.com" });
+ manager
+ .Setup(m => m.GetProfilesForUserAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(new[] { new Profile { Id = 10, Name = "admin" }, new Profile { Id = 20, Name = "user" } });
+
+ var context = BuildAuthenticatedContext("jdoe");
+ var nextCalled = false;
+ var middleware = new ClaimsEnrichmentMiddleware(_ => { nextCalled = true; return Task.CompletedTask; }, NullLogger.Instance, Options);
+
+ await middleware.InvokeAsync(context, manager.Object);
+
+ Assert.True(nextCalled);
+ Assert.Equal("jdoe@example.com", context.User.FindFirst(ClaimTypes.Email)?.Value);
+ Assert.Equal("jdoe", context.User.FindFirst(ClaimTypes.Name)?.Value);
+ Assert.Equal(userId.ToString(), context.User.FindFirst("IdUsuario")?.Value);
+ var roles = context.User.FindAll(ClaimTypes.Role).Select(c => c.Value).ToArray();
+ Assert.Equal(new[] { "10", "20" }, roles);
+ }
+
+ [Fact]
+ public async Task Invoke_MissingUserNameClaim_DoesNotEnrich()
+ {
+ var manager = new Mock(MockBehavior.Strict);
+ var context = BuildAuthenticatedContext(userName: null);
+ var middleware = new ClaimsEnrichmentMiddleware(_ => Task.CompletedTask, NullLogger.Instance, Options);
+
+ await middleware.InvokeAsync(context, manager.Object);
+
+ manager.VerifyNoOtherCalls();
+ }
+
+ [Fact]
+ public async Task Invoke_UserNotFound_DoesNotAddClaims()
+ {
+ var manager = new Mock();
+ manager
+ .Setup(m => m.GetUserAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync((User?)null);
+
+ var context = BuildAuthenticatedContext("ghost");
+ var middleware = new ClaimsEnrichmentMiddleware(_ => Task.CompletedTask, NullLogger.Instance, Options);
+
+ await middleware.InvokeAsync(context, manager.Object);
+
+ Assert.Null(context.User.FindFirst("IdUsuario"));
+ manager.Verify(m => m.GetProfilesForUserAsync(It.IsAny(), It.IsAny()), Times.Never);
+ }
+
+ [Fact]
+ public async Task Invoke_StoreThrows_DegradesGracefullyAndCallsNext()
+ {
+ // The core resilience guarantee: an identity-store failure must never
+ // crash the pipeline.
+ var manager = new Mock();
+ manager
+ .Setup(m => m.GetUserAsync(It.IsAny(), It.IsAny()))
+ .ThrowsAsync(new InvalidOperationException("database is down"));
+
+ var context = BuildAuthenticatedContext("jdoe");
+ var nextCalled = false;
+ var middleware = new ClaimsEnrichmentMiddleware(_ => { nextCalled = true; return Task.CompletedTask; }, NullLogger.Instance, Options);
+
+ var exception = await Record.ExceptionAsync(() => middleware.InvokeAsync(context, manager.Object));
+
+ Assert.Null(exception);
+ Assert.True(nextCalled);
+ Assert.Null(context.User.FindFirst("IdUsuario"));
+ }
+
+ private static DefaultHttpContext BuildAuthenticatedContext(string? userName)
+ {
+ var claims = new List();
+ if (userName is not null)
+ {
+ claims.Add(new Claim("usuario", userName));
+ }
+
+ // A non-null authentication type makes the identity report IsAuthenticated == true.
+ var identity = new ClaimsIdentity(claims, authenticationType: "TestAuth");
+ return new DefaultHttpContext { User = new ClaimsPrincipal(identity) };
+ }
+}
diff --git a/tests/Authorization.UnitTests/ConverterTests.cs b/tests/Authorization.UnitTests/ConverterTests.cs
new file mode 100644
index 0000000..f3554e6
--- /dev/null
+++ b/tests/Authorization.UnitTests/ConverterTests.cs
@@ -0,0 +1,50 @@
+using Authorization.Common;
+using Entities = Authorization.Abstractions.Entities;
+using Models = Authorization.Abstractions.Models;
+
+namespace Authorization.UnitTests;
+
+public class ConverterTests
+{
+ [Fact]
+ public void ConvertList_MapsEveryElement()
+ {
+ var source = new[]
+ {
+ new Entities.Profile { Id = 1, Name = "admin" },
+ new Entities.Profile { Id = 2, Name = "user" },
+ };
+
+ var result = Converter.ConvertList(source);
+
+ Assert.Equal(2, result.Count);
+ Assert.Equal("admin", result[0].Name);
+ Assert.Equal("user", result[1].Name);
+ }
+
+ [Fact]
+ public void ConvertList_EmptySource_ReturnsEmpty()
+ {
+ var result = Converter.ConvertList(Array.Empty());
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void ConvertList_NullSource_Throws()
+ {
+ Assert.Throws(
+ () => Converter.ConvertList(null!));
+ }
+
+ [Fact]
+ public void Clone_ProducesIndependentCopy()
+ {
+ var original = new Models.User { Id = Guid.NewGuid(), UserName = "jdoe" };
+
+ var clone = Converter.Clone(original);
+
+ Assert.NotNull(clone);
+ Assert.NotSame(original, clone);
+ Assert.Equal(original.UserName, clone!.UserName);
+ }
+}
diff --git a/tests/Authorization.UnitTests/GlobalUsings.cs b/tests/Authorization.UnitTests/GlobalUsings.cs
new file mode 100644
index 0000000..c802f44
--- /dev/null
+++ b/tests/Authorization.UnitTests/GlobalUsings.cs
@@ -0,0 +1 @@
+global using Xunit;
diff --git a/tests/Authorization.UnitTests/MapperTests.cs b/tests/Authorization.UnitTests/MapperTests.cs
new file mode 100644
index 0000000..2736756
--- /dev/null
+++ b/tests/Authorization.UnitTests/MapperTests.cs
@@ -0,0 +1,64 @@
+using Authorization.Common;
+using Entities = Authorization.Abstractions.Entities;
+using Models = Authorization.Abstractions.Models;
+
+namespace Authorization.UnitTests;
+
+public class MapperTests
+{
+ [Fact]
+ public void Map_CopiesMatchingProperties()
+ {
+ var id = Guid.NewGuid();
+ var source = new Entities.User
+ {
+ Id = id,
+ UserName = "jdoe",
+ Email = "jdoe@example.com",
+ PasswordHash = "hash",
+ };
+
+ var result = Mapper.Map(source);
+
+ Assert.NotNull(result);
+ Assert.Equal(id, result!.Id);
+ Assert.Equal("jdoe", result.UserName);
+ Assert.Equal("jdoe@example.com", result.Email);
+ Assert.Equal("hash", result.PasswordHash);
+ }
+
+ [Fact]
+ public void Map_NullSource_ReturnsDefault()
+ {
+ var result = Mapper.Map(null);
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void Map_AppliesTransformAfterCopy()
+ {
+ var source = new Entities.Profile { Id = 7, Name = "admin" };
+
+ var result = Mapper.Map(
+ source,
+ (src, dest) => dest.Name = src.Name!.ToUpperInvariant());
+
+ Assert.NotNull(result);
+ Assert.Equal(7, result!.Id);
+ Assert.Equal("ADMIN", result.Name);
+ }
+
+ [Fact]
+ public void Map_IsConsistentAcrossCachedCalls()
+ {
+ // Exercises the per-type-pair property-map cache: a second call must
+ // produce the same result as the first.
+ var first = Mapper.Map(new Entities.Profile { Id = 1, Name = "a" });
+ var second = Mapper.Map(new Entities.Profile { Id = 2, Name = "b" });
+
+ Assert.Equal(1, first!.Id);
+ Assert.Equal("a", first.Name);
+ Assert.Equal(2, second!.Id);
+ Assert.Equal("b", second.Name);
+ }
+}