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