changes: Ai setup
This commit is contained in:
98
CLAUDE.md
Normal file
98
CLAUDE.md
Normal 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
87
GEMINI.md
Normal 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.
|
||||
666
PLAN_OPENIDDICT_IMPLEMENTATION.md
Normal file
666
PLAN_OPENIDDICT_IMPLEMENTATION.md
Normal 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
21
changelog.md
Normal 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
21
plan_diagram.mmd
Normal 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
618
plan_opendict.md
Normal 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
|
||||
Reference in New Issue
Block a user