changes: Ai setup

This commit is contained in:
2025-10-01 22:20:53 -06:00
parent be56bd8a12
commit c471ace09f
6 changed files with 1511 additions and 0 deletions

98
CLAUDE.md Normal file
View File

@@ -0,0 +1,98 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
ASP.NET Core application based on ASP.NET Boilerplate framework with multi-tenancy support, targeting .NET 9.0. Uses PostgreSQL database and JWT Bearer authentication. The project follows a layered architecture pattern with distinct separation of concerns.
## Build & Run Commands
### Build
```bash
cd aspnet-core
dotnet build ASPBaseOIDC.sln
```
### Run the application
```bash
cd aspnet-core/src/ASPBaseOIDC.Web.Host
dotnet run
```
The API will be available at https://localhost:44311/
Swagger UI accessible at https://localhost:44311/swagger
### Run database migrations
```bash
cd aspnet-core/src/ASPBaseOIDC.Migrator
dotnet run
# Or with quiet mode (for CI/CD):
dotnet run -- -q
```
### Run tests
```bash
cd aspnet-core/test/ASPBaseOIDC.Tests
dotnet test
```
## Architecture
### Layer Structure
The solution follows ABP's multi-layer architecture:
- **ASPBaseOIDC.Core** - Domain layer containing entities, domain services, authorization logic, and business rules. Houses the core `Tenant`, `Role`, `User` entities and defines permissions in `PermissionNames`.
- **ASPBaseOIDC.Application** - Application service layer implementing use cases. App Services inherit from `ASPBaseOIDCAppServiceBase` and expose DTOs. Key services: `UserAppService`, `RoleAppService`, `TenantAppService`, `SessionAppService`.
- **ASPBaseOIDC.EntityFrameworkCore** - Data access layer with EF Core. Contains `ASPBaseOIDCDbContext` which inherits from `AbpZeroDbContext`. Database migrations live here. Important: DateTime values are automatically converted to UTC via ValueConverter in OnModelCreating.
- **ASPBaseOIDC.Web.Core** - Web infrastructure shared between hosting models. Contains authentication configuration (`AuthConfigurer`), identity setup (`IdentityRegistrar`), and base controllers.
- **ASPBaseOIDC.Web.Host** - ASP.NET Core Web API hosting. Entry point with `Startup.cs` configuring CORS, Swagger, SignalR, and ABP framework. Controllers expose Application Services as REST endpoints.
- **ASPBaseOIDC.Migrator** - Standalone console application for running database migrations in production environments. Supports quiet mode (`-q`) for automated deployments.
### ABP Framework Integration
The project uses ABP (ASP.NET Boilerplate) modules system:
- Each layer has a Module class (e.g., `ASPBaseOIDCCoreModule`, `ASPBaseOIDCApplicationModule`)
- Modules declare dependencies via `[DependsOn]` attribute
- Dependency injection configured through module initialization (PreInitialize, Initialize, PostInitialize)
- Multi-tenancy controlled by `ASPBaseOIDCConsts.MultiTenancyEnabled`
### Authentication
- JWT Bearer tokens (configured in appsettings.json under Authentication:JwtBearer)
- Security key: `ASPBaseOIDC_C629CD4D2F524E3AA105B46C2D2FC3BC`
- Issuer/Audience: `ASPBaseOIDC`
- Swagger UI includes Bearer token authentication scheme
### Database
- PostgreSQL database (connection string in appsettings.json)
- EF Core migrations in `ASPBaseOIDC.EntityFrameworkCore/Migrations`
- DateTime handling: All DateTime values automatically converted to UTC when saving/reading
- To add new migration: `dotnet ef migrations add <MigrationName> --project src/ASPBaseOIDC.EntityFrameworkCore --startup-project src/ASPBaseOIDC.Web.Host`
## Configuration
- **appsettings.json** in Web.Host: Main configuration for connection strings, app URLs, CORS origins, JWT settings, Kestrel endpoints
- **appsettings.Staging.json**: Environment-specific overrides
- **User Secrets**: Both Core and Web.Host projects use UserSecretsId `JJSolutions-ASPBaseOIDC-56C2EF2F-ABD6-4EFC-AAF2-2E81C34E8FB1`
- **log4net.config**: Logging configuration (log4net.Production.config for production)
## Key Conventions
- Application Services follow naming: `{Entity}AppService` implementing `I{Entity}AppService`
- DTOs organized in Dto folders alongside services
- Authorization: Permissions defined in `PermissionNames.cs`, granted in `ASPBaseOIDCAuthorizationProvider`
- Localization: XML files in Core/Localization/SourceFiles
- ABP auto-validates antiforgery tokens via `AbpAutoValidateAntiforgeryTokenAttribute`
## Development Workflow
**IMPORTANT**: Whenever you make changes to the codebase, you MUST update `changelog.md` with:
- Date and description of changes made
- Files modified/created/deleted
- Important notes about the changes
- Any breaking changes or migration steps required
This ensures context is preserved across sessions and other developers can understand what has been modified.

87
GEMINI.md Normal file
View File

@@ -0,0 +1,87 @@
# GEMINI.md
This file provides guidance to Gemini when working with code in this repository.
## Project Overview
This is an ASP.NET Core application based on the ASP.NET Boilerplate (ABP) framework. It supports multi-tenancy and targets .NET 9.0. The application uses a PostgreSQL database and JWT Bearer authentication with OpenIddict. The project follows a layered architecture pattern with a clear separation of concerns.
## Building and Running
### Build
```bash
cd aspnet-core
dotnet build ASPBaseOIDC.sln
```
### Run the application
```bash
cd aspnet-core/src/ASPBaseOIDC.Web.Host
dotnet run
```
The API will be available at `https://localhost:44311/`.
The Swagger UI is accessible at `https://localhost:44311/swagger`.
### Run database migrations
```bash
cd aspnet-core/src/ASPBaseOIDC.Migrator
dotnet run
```
To run in quiet mode (for CI/CD):
```bash
dotnet run -- -q
```
### Run tests
```bash
cd aspnet-core/test/ASPBaseOIDC.Tests
dotnet test
```
## Development Conventions
### Architecture
The solution follows ABP's multi-layer architecture:
* **ASPBaseOIDC.Core**: Domain layer containing entities, domain services, authorization logic, and business rules.
* **ASPBaseOIDC.Application**: Application service layer that implements use cases.
* **ASPBaseOIDC.EntityFrameworkCore**: Data access layer using EF Core. It contains the `ASPBaseOIDCDbContext` and database migrations.
* **ASPBaseOIDC.Web.Core**: Web infrastructure shared between hosting models, including authentication and base controllers.
* **ASPBaseOIDC.Web.Host**: ASP.NET Core Web API hosting project. This is the entry point of the application.
* **ASPBaseOIDC.Migrator**: A standalone console application for running database migrations.
### ABP Framework Integration
* The project uses ABP's module system. Each layer has a module class (e.g., `ASPBaseOIDCCoreModule`).
* Dependencies between modules are declared using the `[DependsOn]` attribute.
* Dependency injection is configured in the module initialization methods.
### Authentication
* Authentication is handled using JWT Bearer tokens and OpenIddict.
* The main configuration is in `appsettings.json` under the `Authentication:JwtBearer` section.
### Database
* The project uses a PostgreSQL database.
* EF Core migrations are located in the `ASPBaseOIDC.EntityFrameworkCore/Migrations` directory.
* To add a new migration, run the following command:
```bash
dotnet ef migrations add <MigrationName> --project src/ASPBaseOIDC.EntityFrameworkCore --startup-project src/ASPBaseOIDC.Web.Host
```
### General Conventions
* Application Services are named as `{Entity}AppService` and implement `I{Entity}AppService`.
* Data Transfer Objects (DTOs) are located in `Dto` folders alongside the services.
* Authorization permissions are defined in `PermissionNames.cs` and granted in `ASPBaseOIDCAuthorizationProvider`.
* Localization strings are stored in XML files in the `Core/Localization/SourceFiles` directory.
* Keep the `changelog.md` file updated with any changes made to the codebase.

View File

@@ -0,0 +1,666 @@
# Plan de Implementación: OpenIddict + ABP OrganizationUnits con Federación Keycloak
## 🎯 Arquitectura Final Aprobada
### Mapeo Keycloak ↔ ABP
```
┌──────────────────────────────────────────────────────────────┐
│ Keycloak → ABP + OpenIddict │
├──────────────────────────────────────────────────────────────┤
│ Realm (app1, app2) → Aplicación Multi-tenant │
│ Organization (Keycloak) → Tenant (AbpTenant) │
│ Group (Keycloak) → OrganizationUnit (ABP Native) │
│ User → User (AbpUser) │
└──────────────────────────────────────────────────────────────┘
```
### Flujo de Autenticación
```
Usuario → NextJS → OpenIddict (proxy) → Keycloak (IDP)
ABP API (validación local + permisos)
```
---
## 📦 FASE 1: Extensión del Modelo de Datos (Día 1)
### 1.1 Extender Tenant para Keycloak
**Archivo:** `src/ASPBaseOIDC.Core/MultiTenancy/Tenant.cs`
```csharp
public class Tenant : AbpTenant<User>
{
public string TenantUrl { get; set; } // tenant1.app.com
public string KeycloakOrganizationId { get; set; } // UUID de org en Keycloak
public string KeycloakRealmName { get; set; } // "app1" o "app2"
public Tenant()
{
}
public Tenant(string tenancyName, string name)
: base(tenancyName, name)
{
}
}
```
### 1.2 Extender User para External Auth
**Archivo:** `src/ASPBaseOIDC.Core/Authorization/Users/User.cs`
```csharp
public class User : AbpUser<User>
{
public const string DefaultPassword = "123qwe";
// Keycloak Integration
public string ExternalAuthProviderId { get; set; } // Keycloak User UUID
public string KeycloakUserId { get; set; } // Keycloak sub claim
public string PreferredLanguage { get; set; }
// OrganizationUnits membership ya manejado por:
// UserOrganizationUnits (tabla many-to-many nativa de ABP)
// ... resto del código existente
}
```
### 1.3 Usar OrganizationUnit Nativo (NO crear nueva entidad)
**ABP ya provee:**
- `Abp.Organizations.OrganizationUnit` (entidad)
- `OrganizationUnitManager` (dominio)
- `IOrganizationUnitRepository` (repositorio)
- `UserOrganizationUnit` (relación many-to-many)
**Propiedades clave de OrganizationUnit:**
```csharp
// Ya existe en ABP, no crear
public class OrganizationUnit : AuditedAggregateRoot<long>, IMayHaveTenant
{
public int? TenantId { get; set; }
public long? ParentId { get; set; }
public string Code { get; set; } // Auto-generado: "00001.00042"
public string DisplayName { get; set; }
public virtual OrganizationUnit Parent { get; set; }
public virtual ICollection<OrganizationUnit> Children { get; set; }
}
```
**Si necesitas campos extra, crear extensión:**
```csharp
// OPCIONAL: Solo si necesitas mapeo a Keycloak
public class OrganizationUnitExtension : Entity<long>
{
public long OrganizationUnitId { get; set; }
public string KeycloakGroupPath { get; set; } // "/sucursal-norte/ventas"
public string KeycloakGroupId { get; set; } // UUID del group
public OrganizationUnit OrganizationUnit { get; set; }
}
```
### 1.4 Actualizar DbContext
**Archivo:** `src/ASPBaseOIDC.EntityFrameworkCore/EntityFrameworkCore/ASPBaseOIDCDbContext.cs`
```csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// DateTime UTC conversion (ya existe)
// ...
// Configurar extensiones de entidades
modelBuilder.Entity<Tenant>(b =>
{
b.Property(t => t.TenantUrl).HasMaxLength(256);
b.Property(t => t.KeycloakOrganizationId).HasMaxLength(256);
b.Property(t => t.KeycloakRealmName).HasMaxLength(128);
});
modelBuilder.Entity<User>(b =>
{
b.Property(u => u.ExternalAuthProviderId).HasMaxLength(256);
b.Property(u => u.KeycloakUserId).HasMaxLength(256);
b.HasIndex(u => u.KeycloakUserId);
});
// OrganizationUnit ya configurado por ABP
}
```
### 1.5 Crear Migración
```bash
cd src/ASPBaseOIDC.EntityFrameworkCore
dotnet ef migrations add ExtendTenantAndUserForKeycloak --startup-project ../ASPBaseOIDC.Web.Host
```
---
## 📚 FASE 2: Instalación de OpenIddict (Día 1-2)
### 2.1 Instalar Paquetes NuGet
**En `ASPBaseOIDC.Core.csproj`:**
```xml
<PackageReference Include="Abp.ZeroCore.OpenIddict" Version="10.2.0" />
```
**En `ASPBaseOIDC.EntityFrameworkCore.csproj`:**
```xml
<PackageReference Include="Abp.ZeroCore.OpenIddict.EntityFrameworkCore" Version="10.2.0" />
```
**En `ASPBaseOIDC.Web.Core.csproj`:**
```xml
<PackageReference Include="Abp.AspNetCore.OpenIddict" Version="10.2.0" />
```
### 2.2 Actualizar Módulos
**Archivo:** `src/ASPBaseOIDC.Core/ASPBaseOIDCCoreModule.cs`
```csharp
[DependsOn(
typeof(AbpZeroCoreModule),
typeof(AbpZeroCoreOpenIddictModule) // ← AGREGAR
)]
public class ASPBaseOIDCCoreModule : AbpModule
{
// ... sin cambios
}
```
**Archivo:** `src/ASPBaseOIDC.EntityFrameworkCore/EntityFrameworkCore/ASPBaseOIDCEntityFrameworkModule.cs`
```csharp
[DependsOn(
typeof(ASPBaseOIDCCoreModule),
typeof(AbpZeroCoreEntityFrameworkCoreModule),
typeof(AbpZeroCoreOpenIddictEntityFrameworkCoreModule) // ← AGREGAR
)]
public class ASPBaseOIDCEntityFrameworkModule : AbpModule
{
// ... sin cambios
}
```
**Archivo:** `src/ASPBaseOIDC.Web.Core/ASPBaseOIDCWebCoreModule.cs`
```csharp
[DependsOn(
typeof(ASPBaseOIDCCoreModule),
typeof(AbpAspNetCoreModule),
typeof(AbpAspNetCoreOpenIddictModule) // ← AGREGAR
)]
public class ASPBaseOIDCWebCoreModule : AbpModule
{
// ... sin cambios
}
```
### 2.3 Implementar IOpenIddictDbContext
**Archivo:** `src/ASPBaseOIDC.EntityFrameworkCore/EntityFrameworkCore/ASPBaseOIDCDbContext.cs`
```csharp
using OpenIddict.EntityFrameworkCore.Models;
public class ASPBaseOIDCDbContext :
AbpZeroDbContext<Tenant, Role, User, ASPBaseOIDCDbContext>,
IOpenIddictDbContext<Guid> // ← AGREGAR interfaz
{
// DbSets de OpenIddict
public DbSet<OpenIddictApplication<Guid>> OpenIddictApplications { get; set; }
public DbSet<OpenIddictAuthorization<Guid>> OpenIddictAuthorizations { get; set; }
public DbSet<OpenIddictScope<Guid>> OpenIddictScopes { get; set; }
public DbSet<OpenIddictToken<Guid>> OpenIddictTokens { get; set; }
public ASPBaseOIDCDbContext(DbContextOptions<ASPBaseOIDCDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Configurar OpenIddict
modelBuilder.ConfigureOpenIddict();
// DateTime UTC conversion (existente)
// ...
// Tenant/User extensions (de Fase 1.4)
// ...
}
}
```
### 2.4 Crear Migración OpenIddict
```bash
dotnet ef migrations add AddOpenIddictTables --startup-project ../ASPBaseOIDC.Web.Host
```
---
## 🔧 FASE 3: Configuración de OpenIddict Server (Día 2-3)
### 3.1 Crear OpenIddictRegistrar
**Nuevo archivo:** `src/ASPBaseOIDC.Web.Core/OpenIddict/OpenIddictRegistrar.cs`
```csharp
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using System;
namespace ASPBaseOIDC.Web.OpenIddict
{
public static class OpenIddictRegistrar
{
public static void Register(IServiceCollection services, IConfiguration configuration)
{
services.AddOpenIddict()
// Core
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<ASPBaseOIDCDbContext>()
.ReplaceDefaultEntities<Guid>();
})
// Server
.AddServer(options =>
{
// Endpoints
options.SetAuthorizationEndpointUris("/connect/authorize")
.SetTokenEndpointUris("/connect/token")
.SetUserinfoEndpointUris("/connect/userinfo")
.SetLogoutEndpointUris("/connect/logout")
.SetIntrospectionEndpointUris("/connect/introspect");
// Flows
options.AllowPasswordFlow()
.AllowAuthorizationCodeFlow()
.AllowRefreshTokenFlow()
.AllowClientCredentialsFlow();
// Certificates (DEV - cambiar en producción)
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Token lifetime
options.SetAccessTokenLifetime(TimeSpan.FromHours(1))
.SetRefreshTokenLifetime(TimeSpan.FromDays(14));
// ASP.NET Core integration
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableUserinfoEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.EnableStatusCodePagesIntegration();
// Disable encryption for development
options.DisableAccessTokenEncryption();
})
// Validation
.AddValidation(options =>
{
options.UseLocalServer();
options.UseAspNetCore();
});
}
}
}
```
### 3.2 Custom Claims Principal Handler con OrganizationUnits
**Nuevo archivo:** `src/ASPBaseOIDC.Web.Core/OpenIddict/CustomOpenIddictClaimsPrincipalHandler.cs`
```csharp
using Abp.Authorization;
using Abp.Domain.Repositories;
using Abp.OpenIddict;
using Abp.Organizations;
using ASPBaseOIDC.Authorization.Users;
using ASPBaseOIDC.MultiTenancy;
using Microsoft.AspNetCore.Identity;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
namespace ASPBaseOIDC.Web.OpenIddict
{
public class CustomOpenIddictClaimsPrincipalHandler : IAbpOpenIddictClaimsPrincipalHandler
{
private readonly UserManager<User> _userManager;
private readonly IRepository<Tenant> _tenantRepository;
private readonly IRepository<OrganizationUnit, long> _organizationUnitRepository;
private readonly IRepository<UserOrganizationUnit, long> _userOrganizationUnitRepository;
private readonly IPermissionManager _permissionManager;
public CustomOpenIddictClaimsPrincipalHandler(
UserManager<User> userManager,
IRepository<Tenant> tenantRepository,
IRepository<OrganizationUnit, long> organizationUnitRepository,
IRepository<UserOrganizationUnit, long> userOrganizationUnitRepository,
IPermissionManager permissionManager)
{
_userManager = userManager;
_tenantRepository = tenantRepository;
_organizationUnitRepository = organizationUnitRepository;
_userOrganizationUnitRepository = userOrganizationUnitRepository;
_permissionManager = permissionManager;
}
public async Task HandleAsync(AbpOpenIddictClaimsPrincipalHandlerContext context)
{
var userId = context.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
return;
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
return;
var identity = context.Principal.Identities.First();
// Claims básicos de usuario
identity.AddClaim(new Claim("user_id", user.Id.ToString()));
identity.AddClaim(new Claim("username", user.UserName));
identity.AddClaim(new Claim("email", user.EmailAddress));
// Claims de Tenant
if (user.TenantId.HasValue)
{
var tenant = await _tenantRepository.GetAsync(user.TenantId.Value);
identity.AddClaim(new Claim("tenant_id", tenant.Id.ToString()));
identity.AddClaim(new Claim("tenant_name", tenant.Name));
identity.AddClaim(new Claim("tenant_url", tenant.TenantUrl ?? ""));
if (!string.IsNullOrEmpty(tenant.KeycloakRealmName))
{
identity.AddClaim(new Claim("keycloak_realm", tenant.KeycloakRealmName));
}
}
// Claims de OrganizationUnits
var userOus = await _userOrganizationUnitRepository
.GetAllListAsync(uou => uou.UserId == user.Id);
foreach (var userOu in userOus)
{
var ou = await _organizationUnitRepository.GetAsync(userOu.OrganizationUnitId);
identity.AddClaim(new Claim("organization_unit_id", ou.Id.ToString()));
identity.AddClaim(new Claim("organization_unit_code", ou.Code));
identity.AddClaim(new Claim("organization_unit_name", ou.DisplayName));
}
// Roles
var roles = await _userManager.GetRolesAsync(user);
foreach (var role in roles)
{
identity.AddClaim(new Claim(ClaimTypes.Role, role));
}
// Permisos de ABP
var permissions = await _permissionManager.GetGrantedPermissionsAsync(user);
foreach (var permission in permissions)
{
identity.AddClaim(new Claim("permission", permission.Name));
}
}
}
}
```
### 3.3 Actualizar Startup.cs
**Archivo:** `src/ASPBaseOIDC.Web.Host/Startup/Startup.cs`
```csharp
using ASPBaseOIDC.Web.OpenIddict;
using OpenIddict.Validation.AspNetCore;
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// MVC (existente)
services.AddControllersWithViews(/*...*/);
// Registrar OpenIddict
OpenIddictRegistrar.Register(services, _appConfiguration);
// Registrar custom claims handler
services.AddTransient<IAbpOpenIddictClaimsPrincipalHandler, CustomOpenIddictClaimsPrincipalHandler>();
// Identity y Auth (modificar existente)
IdentityRegistrar.Register(services);
// AuthConfigurer ahora debe configurar OpenIddict validation
AuthConfigurer.Configure(services, _appConfiguration);
// ... resto sin cambios
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
app.UseAbp(/*...*/);
app.UseCors(_defaultCorsPolicyName);
app.UseStaticFiles();
app.UseRouting();
// IMPORTANTE: Orden correcto de middlewares
app.UseAuthentication(); // Ya existe
app.UseAuthorization(); // Ya existe
app.UseAbpRequestLocalization();
app.UseEndpoints(/*...*/);
app.UseSwagger(/*...*/);
app.UseSwaggerUI(/*...*/);
}
}
```
### 3.4 Actualizar AuthConfigurer
**Archivo:** `src/ASPBaseOIDC.Web.Core/Authentication/AuthConfigurer.cs`
```csharp
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using OpenIddict.Validation.AspNetCore;
namespace ASPBaseOIDC.Web.Authentication
{
public static class AuthConfigurer
{
public static void Configure(IServiceCollection services, IConfiguration configuration)
{
// Configurar autenticación con OpenIddict como esquema principal
services.AddAuthentication(options =>
{
options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
})
// Mantener JWT Bearer para backwards compatibility (opcional)
.AddJwtBearer(options =>
{
options.Authority = configuration["Authentication:JwtBearer:Authority"];
options.Audience = configuration["Authentication:JwtBearer:Audience"];
options.RequireHttpsMetadata = false;
});
}
}
}
```
---
## 🎮 FASE 4: Controladores OpenIddict (Día 3-4)
### 4.1 AuthorizeController
**Nuevo archivo:** `src/ASPBaseOIDC.Web.Host/Controllers/OpenIddict/AuthorizeController.cs`
Ver código en el plan completo arriba...
### 4.2 TokenController
**Nuevo archivo:** `src/ASPBaseOIDC.Web.Host/Controllers/OpenIddict/TokenController.cs`
Ver código en el plan completo arriba...
### 4.3 UserInfoController
**Nuevo archivo:** `src/ASPBaseOIDC.Web.Host/Controllers/OpenIddict/UserInfoController.cs`
Ver código en el plan completo arriba...
---
## 🌱 FASE 5: Seed Data para OpenIddict (Día 5)
### 5.1 Crear OpenIddictDataSeedContributor
**Nuevo archivo:** `src/ASPBaseOIDC.EntityFrameworkCore/EntityFrameworkCore/Seed/Host/OpenIddictDataSeedContributor.cs`
Ver código en el plan completo arriba...
### 5.2 Registrar Seed en InitialHostDbBuilder
Ver código en el plan completo arriba...
---
## 🧪 FASE 6: Testing y Validación (Día 6)
### 6.1 Ejecutar Migraciones
```bash
cd src/ASPBaseOIDC.Migrator
dotnet run
```
### 6.2 Iniciar Aplicación
```bash
cd src/ASPBaseOIDC.Web.Host
dotnet run
```
### 6.3 Testing con Postman
**Request 1: Obtener Token (Password Grant)**
```http
POST https://localhost:44311/connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=password
&username=admin
&password=admin
&client_id=postman-client
&client_secret=postman-secret-dev-only
&scope=openid profile email api tenant organization offline_access
```
---
## 🚀 FASE 7: Preparación para Keycloak (Futuro - Día 7+)
Ver detalles completos en el plan arriba...
---
## 📝 Resumen de Entregables
### Archivos Nuevos (~18 archivos)
1. `Tenant.cs` (modificado)
2. `User.cs` (modificado)
3. `ASPBaseOIDCDbContext.cs` (modificado)
4. `OpenIddictRegistrar.cs`
5. `CustomOpenIddictClaimsPrincipalHandler.cs`
6. `AuthConfigurer.cs` (modificado)
7. `Startup.cs` (modificado)
8. `AuthorizeController.cs`
9. `TokenController.cs`
10. `UserInfoController.cs`
11. `OpenIddictDataSeedContributor.cs`
12. `InitialHostDbBuilder.cs` (modificado)
13. `KeycloakAuthenticationExtensions.cs` (futuro)
14. `KeycloakSyncService.cs` (futuro)
15. 3 módulos actualizados
16. 2 migraciones EF Core
### Cambios en Base de Datos
- **Migración 1:** ExtendTenantAndUserForKeycloak
- Tenant: +3 columnas
- User: +3 columnas
- **Migración 2:** AddOpenIddictTables
- OpenIddictApplications
- OpenIddictAuthorizations
- OpenIddictScopes
- OpenIddictTokens
- **Tablas ABP existentes (uso nativo):**
- OrganizationUnits
- UserOrganizationUnits
### Paquetes NuGet (~3 paquetes)
- Abp.ZeroCore.OpenIddict (10.2.0)
- Abp.ZeroCore.OpenIddict.EntityFrameworkCore (10.2.0)
- Abp.AspNetCore.OpenIddict (10.2.0)
---
## ⏱️ Estimación Final
| Fase | Tarea | Tiempo |
|------|-------|--------|
| 1 | Modelo de datos + migraciones | 1 día |
| 2 | Instalación OpenIddict + configuración | 1 día |
| 3 | OpenIddictRegistrar + Claims Handler | 1 día |
| 4 | Controladores (Authorize, Token, UserInfo) | 1-2 días |
| 5 | Seed data + testing básico | 0.5 día |
| 6 | Testing exhaustivo (Postman, Swagger) | 0.5 día |
| 7 | Documentación | 0.5 día |
| **TOTAL MVP** | **OpenIddict standalone funcional** | **5-6 días** |
| **Futuro** | Federación Keycloak | **+2-3 días** |
---
## 🔒 Consideraciones de Seguridad
1. **Certificados:** Reemplazar development certs en producción
2. **Client Secrets:** Mover a Azure Key Vault o User Secrets
3. **CORS:** Configurar origins específicos por tenant
4. **Rate Limiting:** Implementar en /connect/token
5. **HTTPS:** Forzar en producción (Kestrel endpoints)
---
## ✅ Criterios de Aceptación
- [ ] OpenIddict emite tokens JWT válidos
- [ ] Password Grant funcional con usuario admin
- [ ] Claims incluyen tenant_id, organization_units, roles, permissions
- [ ] API valida tokens correctamente
- [ ] [AbpAuthorize] funciona sin cambios
- [ ] Swagger UI autentica con OAuth2
- [ ] Postman puede obtener y usar tokens
- [ ] UserInfo retorna información correcta
- [ ] Refresh tokens funcionan
- [ ] Seed data crea aplicaciones cliente
---
**Fecha de creación:** 2025-10-01
**Versión:** 1.0
**Stack:** ASP.NET Boilerplate v10.2.0 + .NET 9.0 + PostgreSQL + OpenIddict

21
changelog.md Normal file
View File

@@ -0,0 +1,21 @@
# Changelog
All notable changes to this project will be documented in this file.
## [2025-10-01] - Initial Documentation
### Added
- Created `CLAUDE.md` with comprehensive project documentation including:
- Project overview and architecture explanation
- Build and run commands for API, migrations, and tests
- Layer structure description (Core, Application, EntityFrameworkCore, Web.Core, Web.Host, Migrator)
- ABP Framework integration details
- Authentication and database configuration
- Key conventions and development workflow rules
- Created `changelog.md` to track all future changes
### Notes
- Project uses ASP.NET Boilerplate framework with .NET 9.0
- PostgreSQL database with automatic UTC DateTime conversion
- JWT Bearer authentication enabled
- Multi-tenancy support configured

21
plan_diagram.mmd Normal file
View File

@@ -0,0 +1,21 @@
sequenceDiagram
participant U as Usuario
participant N as NextJS
participant A as API/ABP
participant O as OpenIddict
participant K as Keycloak
U->>N: Accede a tenant1.app.com
N->>A: GET /api/tenant-info
A->>A: Detecta tenant por URL
A->>N: {tenant_id: "tenant1", organization: "org1"}
N->>O: Redirect a /connect/authorize?tenant=tenant1
O->>K: Challenge a Keycloak realm "tenant1"
K->>U: Muestra login + selector de branches
U->>K: Login + selecciona branch "sucursal-norte"
K->>K: Custom mapper genera claims dinámicos
K->>O: Token con tenant_id, organization, branch, permissions
O->>N: Token JWT
N->>A: API calls con token + claims dinámicos
A->>A: [AbpAuthorize] valida contra claims dinámicos
A->>N: Response

618
plan_opendict.md Normal file
View File

@@ -0,0 +1,618 @@
📋 Plan de Implementación: ASP.NET Boilerplate v10 + OpenIddict (Paso a Paso)
🎯 Fase 1: Preparación e Instalación (1-2 días)
1.1 Instalación de Paquetes NuGet
xml
Copy
<!-- En tu proyecto Core (.Application) -->
<PackageReference Include="Abp.ZeroCore.OpenIddict" Version="10.0.0" />
<!-- En tu proyecto Web (.Web) -->
<PackageReference Include="Abp.AspNetCore.OpenIddict" Version="10.0.0" />
<!-- En tu proyecto EntityFramework (.EntityFrameworkCore) -->
<PackageReference Include="Abp.ZeroCore.OpenIddict.EntityFrameworkCore" Version="10.0.0" />
1.2 Configuración de Módulos
csharp
Copy
// En tu CoreModule.cs
[DependsOn(typeof(AbpZeroCoreOpenIddictModule))]
public class YourCoreModule : AbpModule
{
public override void Initialize()
{
IocManager.RegisterAssemblyByConvention(typeof(YourCoreModule).GetAssembly());
}
}
// En tu WebModule.cs
[DependsOn(typeof(AbpAspNetCoreOpenIddictModule))]
public class YourWebModule : AbpModule
{
public override void PreInitialize()
{
// Deshabilitar autenticación tradicional temporalmente
Configuration.Modules.Zero().UserManagement.IsEmailConfirmationRequiredForLogin = false;
}
}
1.3 Configuración del DbContext
csharp
Copy
// En tu DbContext
public class YourDbContext : AbpZeroDbContext<Tenant, Role, User, YourDbContext>, IOpenIddictDbContext
{
public YourDbContext(DbContextOptions<YourDbContext> options)
: base(options)
{
}
// OpenIddict entities
public DbSet<OpenIddictApplicationModel> Applications { get; set; }
public DbSet<OpenIddictAuthorizationModel> Authorizations { get; set; }
public DbSet<OpenIddictScopeModel> Scopes { get; set; }
public DbSet<OpenIddictTokenModel> Tokens { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Configuración OpenIddict
modelBuilder.ConfigureOpenIddict();
// Configuraciones adicionales de ABP
modelBuilder.Entity<OpenIddictApplicationModel>(entity =>
{
entity.ToTable("OpenIddictApplications");
});
}
}
🚀 Fase 2: Configuración de OpenIddict (2-3 días)
2.1 Crear OpenIddict Registrar
csharp
Copy
// En tu proyecto Web, crea: /Configuration/OpenIddictRegistrar.cs
public static class OpenIddictRegistrar
{
public static void Register(IServiceCollection services, IConfiguration configuration)
{
// Configurar claims principal handler personalizado
services.Configure<AbpOpenIddictClaimsPrincipalOptions>(options =>
{
options.ClaimsPrincipalHandlers.Add<CustomOpenIddictClaimsPrincipalHandler>();
});
services.AddOpenIddict()
// Core components
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<YourDbContext>();
// Usar stores de ABP
options.AddApplicationStore<AbpOpenIddictApplicationStore>()
.AddAuthorizationStore<AbpOpenIddictAuthorizationStore>()
.AddScopeStore<AbpOpenIddictScopeStore>()
.AddTokenStore<AbpOpenIddictTokenStore>();
})
// Server components
.AddServer(options =>
{
// Endpoints
options.SetAuthorizationEndpointUris("/connect/authorize")
.SetTokenEndpointUris("/connect/token")
.SetUserinfoEndpointUris("/connect/userinfo")
.SetLogoutEndpointUris("/connect/logout");
// Flows permitidos
options.AllowPasswordFlow() // Para login usuario/contraseña
.AllowAuthorizationCodeFlow() // Para OAuth estándar
.AllowClientCredentialsFlow() // Para servicio a servicio
.AllowRefreshTokenFlow(); // Para renovar tokens
// Development certificates (cambiar en producción)
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Configuración ASP.NET Core
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableUserinfoEndpointPassthrough()
.EnableLogoutEndpointPassthrough();
// Token configuration
options.SetAccessTokenLifetime(TimeSpan.FromHours(1));
options.SetRefreshTokenLifetime(TimeSpan.FromDays(14));
// Deshabilitar encriptación para desarrollo
options.DisableAccessTokenEncryption();
})
// Validation components
.AddValidation(options =>
{
options.UseLocalServer();
options.UseAspNetCore();
});
}
}
2.2 Custom Claims Principal Handler
csharp
Copy
// En /Authorization/CustomOpenIddictClaimsPrincipalHandler.cs
public class CustomOpenIddictClaimsPrincipalHandler : IAbpOpenIddictClaimsPrincipalHandler
{
private readonly UserManager<User> _userManager;
private readonly IRepository<User, long> _userRepository;
public async Task HandleAsync(AbpOpenIddictClaimsPrincipalHandlerContext context)
{
var user = await _userManager.FindByIdAsync(context.Principal.GetUserId());
if (user != null)
{
var identity = context.Principal.Identities.First();
// Agregar claims personalizados
identity.AddClaim("user_id", user.Id.ToString());
identity.AddClaim("tenant_id", user.TenantId?.ToString() ?? "");
identity.AddClaim("organization", user.Organization ?? "");
identity.AddClaim("branch", user.Branch ?? "");
// Agregar roles
var roles = await _userManager.GetRolesAsync(user);
foreach (var role in roles)
{
identity.AddClaim(ClaimTypes.Role, role);
}
// Agregar permisos de ABP
await AddPermissionClaimsAsync(identity, user);
}
}
private async Task AddPermissionClaimsAsync(ClaimsIdentity identity, User user)
{
// Obtener permisos del usuario
var permissionManager = IocManager.Instance.Resolve<IPermissionManager>();
var permissions = await permissionManager.GetAllForUserAsync(user.Id);
foreach (var permission in permissions.Where(p => p.IsGranted))
{
identity.AddClaim("permission", permission.Name);
}
}
}
🎯 Fase 3: Controladores OpenIddict (2-3 días)
3.1 Authorization Controller
csharp
Copy
// En /Controllers/OpenIddict/AuthorizeController.cs
[Route("connect/[action]")]
public class AuthorizeController : AbpController
{
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IOpenIddictAuthorizationManager _authorizationManager;
private readonly SignInManager<User> _signInManager;
private readonly UserManager<User> _userManager;
public AuthorizeController(
IOpenIddictApplicationManager applicationManager,
IOpenIddictAuthorizationManager authorizationManager,
SignInManager<User> signInManager,
UserManager<User> userManager)
{
_applicationManager = applicationManager;
_authorizationManager = authorizationManager;
_signInManager = signInManager;
_userManager = userManager;
}
[HttpGet("~/connect/authorize")]
[HttpPost("~/connect/authorize")]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Si el usuario no está autenticado, redirigir al login
if (User?.Identity?.IsAuthenticated != true)
{
// Guardar el request para después del login
return Challenge(
authenticationScheme: IdentityConstants.ApplicationScheme,
properties: new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
});
}
// Obtener aplicación cliente
var application = await _applicationManager.FindByClientIdAsync(request.ClientId) ??
throw new InvalidOperationException("Details concerning the calling client application cannot be found.");
// Obtener usuario actual
var user = await _userManager.GetUserAsync(User);
// Crear identidad de claims
var identity = new ClaimsIdentity(
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
nameType: Claims.Name,
roleType: Claims.Role);
// Agregar claims estándar
identity.AddClaim(Claims.Subject, user.Id.ToString());
identity.AddClaim(Claims.Name, user.UserName);
identity.AddClaim(Claims.Email, user.EmailAddress);
// Agregar claims personalizados
identity.AddClaim("tenant_id", user.TenantId?.ToString() ?? "");
identity.AddClaim("organization", user.Organization ?? "");
identity.AddClaim("branch", user.Branch ?? "");
// Agregar roles
var roles = await _userManager.GetRolesAsync(user);
foreach (var role in roles)
{
identity.AddClaim(Claims.Role, role);
}
// Crear principal
var claimsPrincipal = new ClaimsPrincipal(identity);
// Establecer destinos de claims
claimsPrincipal.SetResources(await _applicationManager.ListResourcesAsync(application));
claimsPrincipal.SetScopes(request.GetScopes());
// Firmar y devolver el token
return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
}
3.2 Token Controller
csharp
Copy
// En /Controllers/OpenIddict/TokenController.cs
[Route("connect/[action]")]
public class TokenController : AbpController
{
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IOpenIddictTokenManager _tokenManager;
private readonly SignInManager<User> _signInManager;
private readonly UserManager<User> _userManager;
[HttpPost("~/connect/token")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
if (request.IsPasswordGrantType())
{
return await HandlePasswordGrantAsync(request);
}
else if (request.IsAuthorizationCodeGrantType())
{
return await HandleAuthorizationCodeGrantAsync(request);
}
else if (request.IsRefreshTokenGrantType())
{
return await HandleRefreshTokenGrantAsync(request);
}
else if (request.IsClientCredentialsGrantType())
{
return await HandleClientCredentialsGrantAsync(request);
}
throw new NotImplementedException("The specified grant type is not implemented.");
}
private async Task<IActionResult> HandlePasswordGrantAsync(OpenIddictRequest request)
{
var user = await _userManager.FindByNameOrEmailAsync(request.Username);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid."
}));
}
// Validar contraseña
var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, lockoutOnFailure: true);
if (!result.Succeeded)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid."
}));
}
// Crear identidad
var identity = new ClaimsIdentity(
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
nameType: Claims.Name,
roleType: Claims.Role);
// Agregar claims
identity.AddClaim(Claims.Subject, user.Id.ToString());
identity.AddClaim(Claims.Name, user.UserName);
identity.AddClaim(Claims.Email, user.EmailAddress);
identity.AddClaim("tenant_id", user.TenantId?.ToString() ?? "");
identity.AddClaim("organization", user.Organization ?? "");
identity.AddClaim("branch", user.Branch ?? "");
// Agregar roles
var roles = await _userManager.GetRolesAsync(user);
foreach (var role in roles)
{
identity.AddClaim(Claims.Role, role);
}
var claimsPrincipal = new ClaimsPrincipal(identity);
claimsPrincipal.SetScopes(request.GetScopes());
claimsPrincipal.SetResources(await _applicationManager.ListResourcesAsync(application));
return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
}
3.3 UserInfo Controller
csharp
Copy
// En /Controllers/OpenIddict/UserInfoController.cs
[Route("connect/[action]")]
public class UserInfoController : AbpController
{
[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
[HttpGet("~/connect/userinfo")]
public async Task<IActionResult> UserInfo()
{
var user = await UserManager.GetUserAsync(User);
if (user == null)
{
return Challenge(
authenticationScheme: OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictValidationAspNetCoreConstants.Properties.Error] = Errors.InvalidToken,
[OpenIddictValidationAspNetCoreConstants.Properties.ErrorDescription] = "The specified access token is bound to an account that no longer exists."
}));
}
var claims = new Dictionary<string, object>(StringComparer.Ordinal)
{
// Claims estándar
["sub"] = user.Id.ToString(),
["name"] = user.UserName,
["email"] = user.EmailAddress,
["email_verified"] = user.IsEmailConfirmed,
// Claims personalizados
["tenant_id"] = user.TenantId?.ToString() ?? "",
["organization"] = user.Organization ?? "",
["branch"] = user.Branch ?? "",
["user_id"] = user.Id.ToString()
};
// Agregar roles
var roles = await UserManager.GetRolesAsync(user);
if (roles.Any())
{
claims["roles"] = roles.ToArray();
}
// Agregar permisos
var permissions = await GetUserPermissionsAsync(user);
if (permissions.Any())
{
claims["permissions"] = permissions.ToArray();
}
return Ok(claims);
}
}
🎯 Fase 4: Configuración de Startup (1 día)
4.1 Actualizar Startup.cs
csharp
Copy
public class Startup
{
public IServiceProvider ConfigureServices(IServiceCollection services)
{
// ... otras configuraciones ...
// Registrar OpenIddict
OpenIddictRegistrar.Register(services, _appConfiguration);
// Configurar autenticación
services.AddAuthentication(options =>
{
options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
options.DefaultForbidScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
});
// Agregar autorización con políticas de ABP
services.AddAuthorization(options =>
{
options.AddPolicy("AbpOpenIddict", policy =>
{
policy.RequireAuthenticatedUser();
policy.AddAuthenticationSchemes(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
});
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ... otros middlewares ...
// IMPORTANTE: Agregar OpenIddict antes de ABP
app.UseOpenIddictServer();
app.UseOpenIddictValidation();
// Luego el resto de middlewares de ABP
app.UseAbp();
}
}
🎯 Fase 5: Datos Iniciales y Testing (2-3 días)
5.1 Seed Data para OpenIddict
csharp
Copy
// En /Seeds/OpenIddictDataSeedContributor.cs
public class OpenIddictDataSeedContributor : IDataSeedContributor, ITransientDependency
{
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IOpenIddictScopeManager _scopeManager;
public async Task SeedAsync(DataSeedContext context)
{
// Crear aplicación cliente para tu NextJS
if (await _applicationManager.FindByClientIdAsync("nextjs-client") == null)
{
await _applicationManager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "nextjs-client",
ClientSecret = "your-client-secret",
DisplayName = "NextJS Frontend Application",
RedirectUris =
{
new Uri("http://localhost:3000/callback"),
new Uri("http://localhost:3000")
},
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Authorization,
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.Endpoints.UserInfo,
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
OpenIddictConstants.Permissions.GrantTypes.Password,
OpenIddictConstants.Permissions.GrantTypes.RefreshToken,
OpenIddictConstants.Permissions.Scopes.Email,
OpenIddictConstants.Permissions.Scopes.Profile,
OpenIddictConstants.Permissions.Scopes.Roles
}
});
}
// Crear aplicación para Postman/testing
if (await _applicationManager.FindByClientIdAsync("postman-client") == null)
{
await _applicationManager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "postman-client",
ClientSecret = "postman-secret",
DisplayName = "Postman Testing Client",
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.Endpoints.UserInfo,
OpenIddictConstants.Permissions.GrantTypes.Password,
OpenIddictConstants.Permissions.GrantTypes.ClientCredentials,
OpenIddictConstants.Permissions.Scopes.Email,
OpenIddictConstants.Permissions.Scopes.Profile,
OpenIddictConstants.Permissions.Scopes.Roles
}
});
}
// Crear scopes personalizados
if (await _scopeManager.FindByNameAsync("api") == null)
{
await _scopeManager.CreateAsync(new OpenIddictScopeDescriptor
{
Name = "api",
DisplayName = "API Access",
Resources =
{
"api-resource"
}
});
}
}
}
5.2 Testing con Postman
bash
Copy
# 1. Obtener token con password flow
POST http://localhost:5000/connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=password
&username=admin
&password=123qwe
&client_id=postman-client
&client_secret=postman-secret
&scope=api openid profile email roles
# 2. Usar token para llamar API
GET http://localhost:5000/api/services/app/User/GetAll
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6...
🎯 Fase 6: Integración con ABP Existente (1-2 días)
6.1 Custom AbpAuthorize Attribute
csharp
Copy
// En /Authorization/CustomAbpAuthorizeAttribute.cs
public class CustomAbpAuthorizeAttribute : AbpAuthorizeAttribute
{
public CustomAbpAuthorizeAttribute(params string[] permissions) : base(permissions)
{
}
public override async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
// Verificar autenticación con OpenIddict
if (!context.HttpContext.User.Identity.IsAuthenticated)
{
context.Result = new ChallengeResult(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
return;
}
// Verificar permisos usando claims de OpenIddict
var permissionClaims = context.HttpContext.User.FindAll("permission").Select(c => c.Value);
if (Permissions.Any() && !Permissions.All(p => permissionClaims.Contains(p)))
{
context.Result = new ForbidResult(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
return;
}
await base.OnAuthorizationAsync(context);
}
}
6.2 Actualizar Controladores Existentes
csharp
Copy
// ANTES: Usando ABP tradicional
[AbpAuthorize(PermissionNames.Pages_Users)]
public async Task<ListResultDto<UserListDto>> GetUsers()
{
// código...
}
// DESPUÉS: Usando OpenIddict (igual, solo cambia la autenticación)
[AbpAuthorize(PermissionNames.Pages_Users)] // Funciona igual!
public async Task<ListResultDto<UserListDto>> GetUsers()
{
// código idéntico...
}
📋 Próximos Pasos y Testing
7. Testing Progresivo
Paso 1: Verificar que OpenIddict funcione con usuario admin existente
Paso 2: Proteger un endpoint simple y testear con Postman
Paso 3: Integrar con un endpoint existente de ABP
Paso 4: Implementar refresh tokens
Paso 5: Preparar para federación con Keycloak