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