Files
base_abp/plan_opendict.md
2025-10-01 22:20:53 -06:00

22 KiB

📋 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

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 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(options => { options.ClaimsPrincipalHandlers.Add(); });

    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 _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 _signInManager; private readonly UserManager _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 _signInManager; private readonly UserManager _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 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> GetUsers() { // código... }

// DESPUÉS: Usando OpenIddict (igual, solo cambia la autenticación) [AbpAuthorize(PermissionNames.Pages_Users)] // Funciona igual! public async Task<ListResultDto> 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