diff --git a/Certificado ASP.NET Core Enterprise Application.png b/Certificado ASP.NET Core Enterprise Application.png new file mode 100644 index 0000000..a333cfd Binary files /dev/null and b/Certificado ASP.NET Core Enterprise Application.png differ diff --git "a/Certificado Forma\303\247\303\243o Full Stack Developer.png" "b/Certificado Forma\303\247\303\243o Full Stack Developer.png" new file mode 100644 index 0000000..3debb99 Binary files /dev/null and "b/Certificado Forma\303\247\303\243o Full Stack Developer.png" differ diff --git a/README.md b/README.md index 6975a89..a9f9820 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ - Application Query Stack e Specification Pattern - NetDevPack - Facade +- Elastic Search # Ferramentas - Visual Studio 2022 - Version 17.11.4 ++ diff --git a/src/services/JSE.Identidade.API/Configuration/ApiConfig.cs b/src/services/JSE.Identidade.API/Configuration/ApiConfig.cs index 6ffeef1..2212fc1 100644 --- a/src/services/JSE.Identidade.API/Configuration/ApiConfig.cs +++ b/src/services/JSE.Identidade.API/Configuration/ApiConfig.cs @@ -1,4 +1,6 @@ using JSE.WebAPI.Core.IdentityConfiguration; +using JSE.WebAPI.Core.User; +using NetDevPack.Security.JwtSigningCredentials.AspNetCore; namespace JSE.Identidade.API.Configuration { @@ -8,6 +10,8 @@ public static IServiceCollection AddApiConfiguration(this IServiceCollection ser { services.AddControllers(); + services.AddScoped(); + return services; } @@ -29,6 +33,8 @@ public static IApplicationBuilder UseApiConfiguration(this IApplicationBuilder a endpoints.MapControllers(); }); + app.UseJwksDiscovery(); + return app; } } diff --git a/src/services/JSE.Identidade.API/Configuration/IdentityConfig.cs b/src/services/JSE.Identidade.API/Configuration/IdentityConfig.cs index 20e6db9..92f8b84 100644 --- a/src/services/JSE.Identidade.API/Configuration/IdentityConfig.cs +++ b/src/services/JSE.Identidade.API/Configuration/IdentityConfig.cs @@ -3,6 +3,7 @@ using JSE.WebAPI.Core.IdentityConfiguration; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; +using NetDevPack.Security.JwtSigningCredentials; namespace JSE.Identidade.API.Configuration { @@ -11,6 +12,10 @@ public static class IdentityConfig public static IServiceCollection AddIdentityConfiguration(this IServiceCollection services, IConfiguration configuration) { + + services.AddJwksManager(options => options.Algorithm = Algorithm.ES256) + .PersistKeysToDatabaseStore(); + services.AddDbContext(options => options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"))); diff --git a/src/services/JSE.Identidade.API/Controllers/AuthController.cs b/src/services/JSE.Identidade.API/Controllers/AuthController.cs index 04662dd..ef73d73 100644 --- a/src/services/JSE.Identidade.API/Controllers/AuthController.cs +++ b/src/services/JSE.Identidade.API/Controllers/AuthController.cs @@ -10,6 +10,8 @@ using JSE.MessageBus; using JSE.Core.Messages.Integration; using JSE.WebAPI.Core.IdentityConfiguration; +using JSE.WebAPI.Core.User; +using NetDevPack.Security.JwtSigningCredentials.Interfaces; namespace JSE.Identidade.API.Controllers { @@ -19,18 +21,24 @@ public class AuthController : MainController private readonly SignInManager _signInManager; private readonly UserManager _userManager; private readonly AppSettings _appSettings; + private readonly IAspNetUser _aspNetUser; + private readonly IJsonWebKeySetService _jwksService; private readonly IMessageBus _bus; public AuthController(SignInManager signInManager, UserManager userManager, IOptions appSettings, - IMessageBus bus) + IMessageBus bus, + IAspNetUser aspNetUser, + IJsonWebKeySetService jwksService) { _signInManager = signInManager; _userManager = userManager; _appSettings = appSettings.Value; _bus = bus; + _aspNetUser = aspNetUser; + _jwksService = jwksService; } [HttpPost("nova-conta")] @@ -127,14 +135,16 @@ private async Task ObterClaimsUsuario(ICollection claims, private string CodificarToken(ClaimsIdentity identityClaims) { var tokenHandler = new JwtSecurityTokenHandler(); - var key = Encoding.ASCII.GetBytes(_appSettings.Secret); + + var currentIssuer = $"{_aspNetUser.ObterHttpContext().Request.Scheme}://{_aspNetUser.ObterHttpContext().Request.Host}"; + + var key = _jwksService.GetCurrent(); var token = tokenHandler.CreateToken(new SecurityTokenDescriptor { - Issuer = _appSettings.Issuer, - Audience = _appSettings.ValidOn, + Issuer = currentIssuer, Subject = identityClaims, - Expires = DateTime.UtcNow.AddHours(_appSettings.ExpirationHours), - SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = key }); return tokenHandler.WriteToken(token); diff --git a/src/services/JSE.Identidade.API/Data/ApplicationDbContext.cs b/src/services/JSE.Identidade.API/Data/ApplicationDbContext.cs index ec586c2..af260b1 100644 --- a/src/services/JSE.Identidade.API/Data/ApplicationDbContext.cs +++ b/src/services/JSE.Identidade.API/Data/ApplicationDbContext.cs @@ -1,10 +1,14 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; +using NetDevPack.Security.JwtSigningCredentials; +using NetDevPack.Security.JwtSigningCredentials.Store.EntityFrameworkCore; namespace JSE.Identidade.API.Data { - public class ApplicationDbContext : IdentityDbContext + public class ApplicationDbContext : IdentityDbContext, ISecurityKeyContext { public ApplicationDbContext(DbContextOptions options) : base(options) { } + + public DbSet SecurityKeys { get; set; } } } diff --git a/src/services/JSE.Identidade.API/JSE.Identidade.API.csproj b/src/services/JSE.Identidade.API/JSE.Identidade.API.csproj index 976a94d..4ce40f4 100644 --- a/src/services/JSE.Identidade.API/JSE.Identidade.API.csproj +++ b/src/services/JSE.Identidade.API/JSE.Identidade.API.csproj @@ -16,6 +16,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/src/services/JSE.Identidade.API/Migrations/20250114201822_SecKeys.Designer.cs b/src/services/JSE.Identidade.API/Migrations/20250114201822_SecKeys.Designer.cs new file mode 100644 index 0000000..f7dc872 --- /dev/null +++ b/src/services/JSE.Identidade.API/Migrations/20250114201822_SecKeys.Designer.cs @@ -0,0 +1,309 @@ +// +using System; +using JSE.Identidade.API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace JSE.Identidade.API.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250114201822_SecKeys")] + partial class SecKeys + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProviderKey") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("NetDevPack.Security.JwtSigningCredentials.SecurityKeyWithPrivate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Algorithm") + .HasColumnType("nvarchar(max)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("KeyId") + .HasColumnType("nvarchar(max)"); + + b.Property("Parameters") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("SecurityKeys"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/services/JSE.Identidade.API/Migrations/20250114201822_SecKeys.cs b/src/services/JSE.Identidade.API/Migrations/20250114201822_SecKeys.cs new file mode 100644 index 0000000..d5bed13 --- /dev/null +++ b/src/services/JSE.Identidade.API/Migrations/20250114201822_SecKeys.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace JSE.Identidade.API.Migrations +{ + /// + public partial class SecKeys : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "SecurityKeys", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Parameters = table.Column(type: "nvarchar(max)", nullable: true), + KeyId = table.Column(type: "nvarchar(max)", nullable: true), + Type = table.Column(type: "nvarchar(max)", nullable: true), + Algorithm = table.Column(type: "nvarchar(max)", nullable: true), + CreationDate = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SecurityKeys", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SecurityKeys"); + } + } +} diff --git a/src/services/JSE.Identidade.API/Migrations/ApplicationDbContextModelSnapshot.cs b/src/services/JSE.Identidade.API/Migrations/ApplicationDbContextModelSnapshot.cs index b7915a6..92a12c5 100644 --- a/src/services/JSE.Identidade.API/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/services/JSE.Identidade.API/Migrations/ApplicationDbContextModelSnapshot.cs @@ -224,6 +224,32 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUserTokens", (string)null); }); + modelBuilder.Entity("NetDevPack.Security.JwtSigningCredentials.SecurityKeyWithPrivate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Algorithm") + .HasColumnType("nvarchar(max)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("KeyId") + .HasColumnType("nvarchar(max)"); + + b.Property("Parameters") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("SecurityKeys"); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)