diff --git a/Certificado ASP.NET Core Enterprise Application.png b/Certificado ASP.NET Core Enterprise Application.png deleted file mode 100644 index a333cfd..0000000 Binary files a/Certificado ASP.NET Core Enterprise Application.png and /dev/null 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" deleted file mode 100644 index 3debb99..0000000 Binary files "a/Certificado Forma\303\247\303\243o Full Stack Developer.png" and /dev/null differ diff --git a/src/services/JSE.Identidade.API/Configuration/ApiConfig.cs b/src/services/JSE.Identidade.API/Configuration/ApiConfig.cs index 2212fc1..fccb00e 100644 --- a/src/services/JSE.Identidade.API/Configuration/ApiConfig.cs +++ b/src/services/JSE.Identidade.API/Configuration/ApiConfig.cs @@ -1,4 +1,5 @@ -using JSE.WebAPI.Core.IdentityConfiguration; +using JSE.Identidade.API.Services; +using JSE.WebAPI.Core.IdentityConfiguration; using JSE.WebAPI.Core.User; using NetDevPack.Security.JwtSigningCredentials.AspNetCore; @@ -10,6 +11,7 @@ public static IServiceCollection AddApiConfiguration(this IServiceCollection ser { services.AddControllers(); + services.AddScoped(); services.AddScoped(); return services; diff --git a/src/services/JSE.Identidade.API/Configuration/IdentityConfig.cs b/src/services/JSE.Identidade.API/Configuration/IdentityConfig.cs index 6fa7604..e4faa15 100644 --- a/src/services/JSE.Identidade.API/Configuration/IdentityConfig.cs +++ b/src/services/JSE.Identidade.API/Configuration/IdentityConfig.cs @@ -12,6 +12,9 @@ public static IServiceCollection AddIdentityConfiguration(this IServiceCollectio IConfiguration configuration) { + var appSettingsSection = configuration.GetSection("AppTokenSettings"); + services.Configure(appSettingsSection); + services.AddJwksManager(options => options.Algorithm = Algorithm.ES256) .PersistKeysToDatabaseStore(); diff --git a/src/services/JSE.Identidade.API/Controllers/AuthController.cs b/src/services/JSE.Identidade.API/Controllers/AuthController.cs index 728503a..84b18df 100644 --- a/src/services/JSE.Identidade.API/Controllers/AuthController.cs +++ b/src/services/JSE.Identidade.API/Controllers/AuthController.cs @@ -1,44 +1,29 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Text; +using System; +using System.Threading.Tasks; +using JSE.Core.Messages.Integration; +using JSE.MessageBus; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; +using JSE.Core.Messages.Integration; using JSE.Identidade.API.Models; -using JSE.WebAPI.Core.Controllers; +using JSE.Identidade.API.Services; using JSE.MessageBus; -using JSE.Core.Messages.Integration; -using JSE.WebAPI.Core.IdentityConfiguration; -using JSE.WebAPI.Core.User; -using NetDevPack.Security.JwtSigningCredentials.Interfaces; +using JSE.WebAPI.Core.Controllers; namespace JSE.Identidade.API.Controllers { [Route("api/identidade")] 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 AuthenticationService _authenticationService; private readonly IMessageBus _bus; - public AuthController(SignInManager signInManager, - UserManager userManager, - IOptions appSettings, - IMessageBus bus, - IAspNetUser aspNetUser, - IJsonWebKeySetService jwksService) + public AuthController( + AuthenticationService authenticationService, + IMessageBus bus) { - _signInManager = signInManager; - _userManager = userManager; - _appSettings = appSettings.Value; + _authenticationService = authenticationService; _bus = bus; - _aspNetUser = aspNetUser; - _jwksService = jwksService; } [HttpPost("nova-conta")] @@ -53,19 +38,19 @@ public async Task Registrar(UsuarioRegistroViewModel usuarioRegist EmailConfirmed = true }; - var result = await _userManager.CreateAsync(user, usuarioRegistro.Senha); + var result = await _authenticationService.UserManager.CreateAsync(user, usuarioRegistro.Senha); if (result.Succeeded) { var clienteResult = await RegistrarCliente(usuarioRegistro); - if(!clienteResult.ValidationResult.IsValid) + if (!clienteResult.ValidationResult.IsValid) { - await _userManager.DeleteAsync(user); + await _authenticationService.UserManager.DeleteAsync(user); return CustomResponse(clienteResult.ValidationResult); } - return CustomResponse(await GerarJwt(usuarioRegistro.Email)); + return CustomResponse(await _authenticationService.GerarJwt(usuarioRegistro.Email)); } foreach (var error in result.Errors) @@ -79,15 +64,14 @@ public async Task Registrar(UsuarioRegistroViewModel usuarioRegist [HttpPost("autenticar")] public async Task Login(UsuarioLoginViewModel usuarioLogin) { - if (!ModelState.IsValid) return CustomResponse(ModelState); - var result = await _signInManager.PasswordSignInAsync(usuarioLogin.Email, usuarioLogin.Senha, + var result = await _authenticationService.SignInManager.PasswordSignInAsync(usuarioLogin.Email, usuarioLogin.Senha, false, true); if (result.Succeeded) { - return CustomResponse(await GerarJwt(usuarioLogin.Email)); + return CustomResponse(await _authenticationService.GerarJwt(usuarioLogin.Email)); } if (result.IsLockedOut) @@ -100,91 +84,42 @@ public async Task Login(UsuarioLoginViewModel usuarioLogin) return CustomResponse(); } - private async Task GerarJwt(string email) - { - var user = await _userManager.FindByEmailAsync(email); - var claims = await _userManager.GetClaimsAsync(user); - - var identityClaims = await ObterClaimsUsuario(claims, user); - var encodedToken = CodificarToken(identityClaims); - - return ObterRespostaToken(encodedToken, user, claims); - } - - private async Task ObterClaimsUsuario(ICollection claims, IdentityUser user) + private async Task RegistrarCliente(UsuarioRegistroViewModel usuarioRegistro) { - var userRoles = await _userManager.GetRolesAsync(user); + var usuario = await _authenticationService.UserManager.FindByEmailAsync(usuarioRegistro.Email); - claims.Add(new Claim(JwtRegisteredClaimNames.Sub, user.Id)); - claims.Add(new Claim(JwtRegisteredClaimNames.Email, user.Email)); - claims.Add(new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())); - claims.Add(new Claim(JwtRegisteredClaimNames.Nbf, ToUnixEpochDate(DateTime.UtcNow).ToString())); - claims.Add(new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(DateTime.UtcNow).ToString(), ClaimValueTypes.Integer64)); + var usuarioRegistrado = new UsuarioRegistradoIntegrationEvent( + Guid.Parse(usuario.Id), usuarioRegistro.Nome, usuarioRegistro.Email, usuarioRegistro.Cpf); - foreach (var userRole in userRoles) + try { - claims.Add(new Claim("role", userRole)); + return await _bus.RequestAsync(usuarioRegistrado); } - - var identityClaims = new ClaimsIdentity(); - identityClaims.AddClaims(claims); - - return identityClaims; - } - - private string CodificarToken(ClaimsIdentity identityClaims) - { - var tokenHandler = new JwtSecurityTokenHandler(); - - var currentIssuer = $"{_aspNetUser.ObterHttpContext().Request.Scheme}://{_aspNetUser.ObterHttpContext().Request.Host}"; - - var key = _jwksService.GetCurrent(); - var token = tokenHandler.CreateToken(new SecurityTokenDescriptor + catch { - Issuer = currentIssuer, - Subject = identityClaims, - Expires = DateTime.UtcNow.AddHours(1), - SigningCredentials = key - }); - - return tokenHandler.WriteToken(token); + await _authenticationService.UserManager.DeleteAsync(usuario); + throw; + } } - private UsuarioRespostaLoginViewModel ObterRespostaToken(string encodedToken, IdentityUser user, IEnumerable claims) + [HttpPost("refresh-token")] + public async Task RefreshToken([FromBody] string refreshToken) { - return new UsuarioRespostaLoginViewModel + if (string.IsNullOrEmpty(refreshToken)) { - AccessToken = encodedToken, - ExpiresIn = TimeSpan.FromHours(1).TotalSeconds, - UsuarioToken = new UsuarioTokenViewModel - { - Id = user.Id, - Email = user.Email, - Claims = claims.Select(c => new UsuarioClaimViewModel { Type = c.Type, Value = c.Value }) - } - }; - } - - private static long ToUnixEpochDate(DateTime date) - => (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds); - - private async Task RegistrarCliente(UsuarioRegistroViewModel usuarioRegistro) - { - var usuario = await _userManager.FindByEmailAsync(usuarioRegistro.Email); - - var usuarioRegistrado = new UsuarioRegistradoIntegrationEvent( - Guid.Parse(usuario.Id), usuarioRegistro.Nome, usuarioRegistro.Email, usuarioRegistro.Cpf); + AddProcessingError("Refresh Token inválido"); + return CustomResponse(); + } + var token = await _authenticationService.ObterRefreshToken(Guid.Parse(refreshToken)); - try - { - return await _bus.RequestAsync(usuarioRegistrado); - } - catch + if (token is null) { - await _userManager.DeleteAsync(usuario); - throw; + AddProcessingError("Refresh Token expirado"); + return CustomResponse(); } + + return CustomResponse(await _authenticationService.GerarJwt(token.UserName)); } } } \ No newline at end of file diff --git a/src/services/JSE.Identidade.API/Data/ApplicationDbContext.cs b/src/services/JSE.Identidade.API/Data/ApplicationDbContext.cs index af260b1..1ede1a1 100644 --- a/src/services/JSE.Identidade.API/Data/ApplicationDbContext.cs +++ b/src/services/JSE.Identidade.API/Data/ApplicationDbContext.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using JSE.Identidade.API.Models; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using NetDevPack.Security.JwtSigningCredentials; using NetDevPack.Security.JwtSigningCredentials.Store.EntityFrameworkCore; @@ -10,5 +11,7 @@ public class ApplicationDbContext : IdentityDbContext, ISecurityKeyContext public ApplicationDbContext(DbContextOptions options) : base(options) { } public DbSet SecurityKeys { get; set; } + + public DbSet RefreshTokens { get; set; } } } diff --git a/src/services/JSE.Identidade.API/Extensions/AppTokenSettings.cs b/src/services/JSE.Identidade.API/Extensions/AppTokenSettings.cs new file mode 100644 index 0000000..87fdcb9 --- /dev/null +++ b/src/services/JSE.Identidade.API/Extensions/AppTokenSettings.cs @@ -0,0 +1,7 @@ +namespace JSE.Identidade.API.Extensions +{ + public class AppTokenSettings + { + public int RefreshTokenExpiration { get; set; } + } +} diff --git a/src/services/JSE.Identidade.API/Migrations/20250115141850_RefreshToken.Designer.cs b/src/services/JSE.Identidade.API/Migrations/20250115141850_RefreshToken.Designer.cs new file mode 100644 index 0000000..86fce9e --- /dev/null +++ b/src/services/JSE.Identidade.API/Migrations/20250115141850_RefreshToken.Designer.cs @@ -0,0 +1,330 @@ +// +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("20250115141850_RefreshToken")] + partial class RefreshToken + { + /// + 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("JSE.Identidade.API.Models.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ExpirationDate") + .HasColumnType("datetime2"); + + b.Property("Token") + .HasColumnType("uniqueidentifier"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("RefreshTokens"); + }); + + 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/20250115141850_RefreshToken.cs b/src/services/JSE.Identidade.API/Migrations/20250115141850_RefreshToken.cs new file mode 100644 index 0000000..d9e0bdd --- /dev/null +++ b/src/services/JSE.Identidade.API/Migrations/20250115141850_RefreshToken.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace JSE.Identidade.API.Migrations +{ + /// + public partial class RefreshToken : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "RefreshTokens", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + UserName = table.Column(type: "nvarchar(max)", nullable: false), + Token = table.Column(type: "uniqueidentifier", nullable: false), + ExpirationDate = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RefreshTokens", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RefreshTokens"); + } + } +} diff --git a/src/services/JSE.Identidade.API/Migrations/ApplicationDbContextModelSnapshot.cs b/src/services/JSE.Identidade.API/Migrations/ApplicationDbContextModelSnapshot.cs index 92a12c5..d1068ab 100644 --- a/src/services/JSE.Identidade.API/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/services/JSE.Identidade.API/Migrations/ApplicationDbContextModelSnapshot.cs @@ -22,6 +22,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + modelBuilder.Entity("JSE.Identidade.API.Models.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ExpirationDate") + .HasColumnType("datetime2"); + + b.Property("Token") + .HasColumnType("uniqueidentifier"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("RefreshTokens"); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => { b.Property("Id") diff --git a/src/services/JSE.Identidade.API/Models/RefreshToken.cs b/src/services/JSE.Identidade.API/Models/RefreshToken.cs new file mode 100644 index 0000000..409bd3e --- /dev/null +++ b/src/services/JSE.Identidade.API/Models/RefreshToken.cs @@ -0,0 +1,17 @@ +namespace JSE.Identidade.API.Models +{ + public class RefreshToken + { + public RefreshToken() + { + Id = Guid.NewGuid(); + Token = Guid.NewGuid(); + } + + public Guid Id { get; set; } + public string UserName { get; set; } + public Guid Token { get; set; } + public DateTime ExpirationDate { get; set; } + + } +} diff --git a/src/services/JSE.Identidade.API/Models/UsuarioRespostaLoginViewModel.cs b/src/services/JSE.Identidade.API/Models/UsuarioRespostaLoginViewModel.cs index ea97931..a8a9776 100644 --- a/src/services/JSE.Identidade.API/Models/UsuarioRespostaLoginViewModel.cs +++ b/src/services/JSE.Identidade.API/Models/UsuarioRespostaLoginViewModel.cs @@ -5,5 +5,6 @@ public class UsuarioRespostaLoginViewModel public string AccessToken { get; set; } public double ExpiresIn { get; set; } public UsuarioTokenViewModel UsuarioToken { get; set; } + public Guid RefreshToken { get; set; } } } diff --git a/src/services/JSE.Identidade.API/Services/AuthenticationService.cs b/src/services/JSE.Identidade.API/Services/AuthenticationService.cs new file mode 100644 index 0000000..eb11e74 --- /dev/null +++ b/src/services/JSE.Identidade.API/Services/AuthenticationService.cs @@ -0,0 +1,142 @@ +using JSE.Identidade.API.Data; +using JSE.Identidade.API.Extensions; +using JSE.Identidade.API.Models; +using JSE.WebAPI.Core.User; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using NetDevPack.Security.JwtSigningCredentials.Interfaces; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; + +namespace JSE.Identidade.API.Services +{ + public class AuthenticationService + { + public readonly SignInManager SignInManager; + public readonly UserManager UserManager; + private readonly AppSettings _appSettings; + private readonly AppTokenSettings _appTokenSettingsSettings; + private readonly ApplicationDbContext _context; + + private readonly IJsonWebKeySetService _jwksService; + private readonly IAspNetUser _aspNetUser; + + public AuthenticationService( + SignInManager signInManager, + UserManager userManager, + IOptions appSettings, + IOptions appTokenSettingsSettings, + ApplicationDbContext context, + IJsonWebKeySetService jwksService, + IAspNetUser aspNetUser) + { + SignInManager = signInManager; + UserManager = userManager; + _appSettings = appSettings.Value; + _appTokenSettingsSettings = appTokenSettingsSettings.Value; + _jwksService = jwksService; + _aspNetUser = aspNetUser; + _context = context; + } + + public async Task GerarJwt(string email) + { + var user = await UserManager.FindByEmailAsync(email); + var claims = await UserManager.GetClaimsAsync(user); + + var identityClaims = await ObterClaimsUsuario(claims, user); + var encodedToken = CodificarToken(identityClaims); + + var refreshToken = await GerarRefreshToken(email); + + return ObterRespostaToken(encodedToken, user, claims, refreshToken); + } + + private async Task ObterClaimsUsuario(ICollection claims, IdentityUser user) + { + var userRoles = await UserManager.GetRolesAsync(user); + + claims.Add(new Claim(JwtRegisteredClaimNames.Sub, user.Id)); + claims.Add(new Claim(JwtRegisteredClaimNames.Email, user.Email)); + claims.Add(new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())); + claims.Add(new Claim(JwtRegisteredClaimNames.Nbf, ToUnixEpochDate(DateTime.UtcNow).ToString())); + claims.Add(new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(DateTime.UtcNow).ToString(), + ClaimValueTypes.Integer64)); + foreach (var userRole in userRoles) + { + claims.Add(new Claim("role", userRole)); + } + + var identityClaims = new ClaimsIdentity(); + identityClaims.AddClaims(claims); + + return identityClaims; + } + + private string CodificarToken(ClaimsIdentity identityClaims) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var currentIssuer = + $"{_aspNetUser.ObterHttpContext().Request.Scheme}://{_aspNetUser.ObterHttpContext().Request.Host}"; + var key = _jwksService.GetCurrent(); + var token = tokenHandler.CreateToken(new SecurityTokenDescriptor + { + Issuer = currentIssuer, + Subject = identityClaims, + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = key + }); + + return tokenHandler.WriteToken(token); + } + + private UsuarioRespostaLoginViewModel ObterRespostaToken(string encodedToken, IdentityUser user, + IEnumerable claims, RefreshToken refreshToken) + { + return new UsuarioRespostaLoginViewModel + { + AccessToken = encodedToken, + RefreshToken = refreshToken.Token, + ExpiresIn = TimeSpan.FromHours(1).TotalSeconds, + UsuarioToken = new UsuarioTokenViewModel + { + Id = user.Id, + Email = user.Email, + Claims = claims.Select(c => new UsuarioClaimViewModel { Type = c.Type, Value = c.Value }) + } + }; + } + + private static long ToUnixEpochDate(DateTime date) + => (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)) + .TotalSeconds); + + private async Task GerarRefreshToken(string email) + { + var refreshToken = new RefreshToken + { + UserName = email, + ExpirationDate = DateTime.UtcNow.AddHours(_appTokenSettingsSettings.RefreshTokenExpiration) + }; + + _context.RefreshTokens.RemoveRange(_context.RefreshTokens.Where(u => u.UserName == email)); + await _context.RefreshTokens.AddAsync(refreshToken); + + await _context.SaveChangesAsync(); + + return refreshToken; + } + + public async Task ObterRefreshToken(Guid refreshToken) + { + var token = await _context.RefreshTokens.AsNoTracking() + .FirstOrDefaultAsync(u => u.Token == refreshToken); + + return token != null && token.ExpirationDate.ToLocalTime() > DateTime.Now + ? token + : null; + } + } +} \ No newline at end of file diff --git a/src/services/JSE.Identidade.API/appsettings.Development.json b/src/services/JSE.Identidade.API/appsettings.Development.json index b06a22d..809dc53 100644 --- a/src/services/JSE.Identidade.API/appsettings.Development.json +++ b/src/services/JSE.Identidade.API/appsettings.Development.json @@ -12,5 +12,10 @@ "MessageQueueConnection": { "MessageBus": "host=localhost:5672;publisherConfirms=true;timeout=10" + }, + + "AppTokenSettings": { + "RefreshTokenExpiration": 8 } + }