From 3da900a30e788d0acf2fcee7dba2aecdb16aab43 Mon Sep 17 00:00:00 2001 From: HombreLaser Date: Sun, 27 Nov 2022 12:46:26 -0600 Subject: Añadido logout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Controllers/RafflesController.cs | 2 +- Controllers/UserAccountSessionsController.cs | 15 +- Logics/BaseUserAccountLogic.cs | 2 + Logics/DestroyUserAccountSessionLogic.cs | 30 ++ Logics/RefreshTokenLogic.cs | 1 + ...0221127165449_AddCurrentTokenToUser.Designer.cs | 468 +++++++++++++++++++++ Migrations/20221127165449_AddCurrentTokenToUser.cs | 28 ++ Migrations/ApplicationDbContextModelSnapshot.cs | 7 +- Models/UserAccount.cs | 1 + Policies/CorrectTokenHandler.cs | 29 ++ Policies/CorrectTokenRequirement.cs | 5 + Program.cs | 8 + 12 files changed, 593 insertions(+), 3 deletions(-) create mode 100644 Logics/DestroyUserAccountSessionLogic.cs create mode 100644 Migrations/20221127165449_AddCurrentTokenToUser.Designer.cs create mode 100644 Migrations/20221127165449_AddCurrentTokenToUser.cs create mode 100644 Policies/CorrectTokenHandler.cs create mode 100644 Policies/CorrectTokenRequirement.cs diff --git a/Controllers/RafflesController.cs b/Controllers/RafflesController.cs index c649b70..aff4085 100644 --- a/Controllers/RafflesController.cs +++ b/Controllers/RafflesController.cs @@ -68,7 +68,7 @@ namespace BackendPIA.Controllers { return StatusCode(303, new { Message = "The resource has been deleted"} ); } - [Authorize] + [Authorize(Policy = "ValidToken")] [HttpGet("{id:int}/available_tickets")] public async Task>> AvailableTickets(long id) { IEnumerable available_tickets = from number in Enumerable.Range(1, 54) select number; diff --git a/Controllers/UserAccountSessionsController.cs b/Controllers/UserAccountSessionsController.cs index 217c05c..ebeca96 100644 --- a/Controllers/UserAccountSessionsController.cs +++ b/Controllers/UserAccountSessionsController.cs @@ -32,11 +32,24 @@ namespace BackendPIA.Controllers { return StatusCode(401, new InvalidLoginError(401, "Check your credentials")); } + [Authorize(Policy = "ValidToken")] + [HttpDelete("logout")] + public async Task Delete() { + string email = HttpContext.User.Claims.Where(c => c.Type.Contains("email")).First().Value; + DestroyUserAccountSessionLogic logic = new DestroyUserAccountSessionLogic(_manager, email); + bool result = await logic.Call(); + + if(result) + return Ok(); + + return NotFound(new NotFoundError(404, "Couldn't find the user.")); + } + // [Authorize] [HttpPost("refresh")] public async Task> Refresh(AuthenticationToken form) { RefreshTokenLogic logic = new RefreshTokenLogic(_token_generator, _manager, form); - var result = await logic.Call(); + bool result = await logic.Call(); if(result) return Ok(logic.Token); diff --git a/Logics/BaseUserAccountLogic.cs b/Logics/BaseUserAccountLogic.cs index 4ce17e0..43a8ed9 100644 --- a/Logics/BaseUserAccountLogic.cs +++ b/Logics/BaseUserAccountLogic.cs @@ -19,6 +19,8 @@ namespace BackendPIA.Logics { var roles = await _manager.GetRolesAsync(user); _token = new AuthenticationToken { Token = _token_generator.Generate(user, roles[0]), RefreshToken = _token_generator.GenerateRefreshToken() }; + user.CurrentToken = _token.Token; + await _manager.UpdateAsync(user); } // We overwrite or set the value of the session token in the database: all other previous logins are invalid. diff --git a/Logics/DestroyUserAccountSessionLogic.cs b/Logics/DestroyUserAccountSessionLogic.cs new file mode 100644 index 0000000..1e5a5f5 --- /dev/null +++ b/Logics/DestroyUserAccountSessionLogic.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Identity; +using BackendPIA.Services; +using BackendPIA.Models; +using BackendPIA.Forms; + +namespace BackendPIA.Logics { + public class DestroyUserAccountSessionLogic { + private readonly UserManager _manager; + private readonly string _email; + + public DestroyUserAccountSessionLogic(UserManager manager, string email) { + _manager = manager; + _email = email; + } + + public async Task Call() { + var user = await _manager.FindByEmailAsync(_email); + + if(user == null) + return false; + + user.SessionToken = null; + user.CurrentToken = null; + user.SessionTokenExpiryTime = null; + await _manager.UpdateAsync(user); + + return true; + } + } +} \ No newline at end of file diff --git a/Logics/RefreshTokenLogic.cs b/Logics/RefreshTokenLogic.cs index 200438a..3493f47 100644 --- a/Logics/RefreshTokenLogic.cs +++ b/Logics/RefreshTokenLogic.cs @@ -26,6 +26,7 @@ namespace BackendPIA.Logics { || user.SessionToken == null || user.SessionToken != _form.RefreshToken) { user.SessionToken = null; user.SessionTokenExpiryTime = null; + user.CurrentToken = null; _manager.UpdateAsync(user); return false; diff --git a/Migrations/20221127165449_AddCurrentTokenToUser.Designer.cs b/Migrations/20221127165449_AddCurrentTokenToUser.Designer.cs new file mode 100644 index 0000000..3576dd7 --- /dev/null +++ b/Migrations/20221127165449_AddCurrentTokenToUser.Designer.cs @@ -0,0 +1,468 @@ +// +using System; +using BackendPIA.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace BackendPIA.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20221127165449_AddCurrentTokenToUser")] + partial class AddCurrentTokenToUser + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("BackendPIA.Models.Prize", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RaffleId") + .HasColumnType("bigint"); + + b.Property("Tier") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RaffleId"); + + b.ToTable("Prizes"); + }); + + modelBuilder.Entity("BackendPIA.Models.Raffle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IsClosed") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Winners") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Raffles"); + }); + + modelBuilder.Entity("BackendPIA.Models.RaffleWinner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PrizeId") + .HasColumnType("bigint"); + + b.Property("RaffleId") + .HasColumnType("bigint"); + + b.Property("UserAccountId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("PrizeId"); + + b.HasIndex("RaffleId"); + + b.HasIndex("UserAccountId"); + + b.ToTable("RaffleWinners"); + }); + + modelBuilder.Entity("BackendPIA.Models.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IsWinner") + .HasColumnType("boolean"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("RaffleId") + .HasColumnType("bigint"); + + b.Property("UserAccountId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RaffleId"); + + b.HasIndex("UserAccountId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("BackendPIA.Models.UserAccount", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CurrentToken") + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("SessionToken") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SessionTokenExpiryTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("BackendPIA.Models.Prize", b => + { + b.HasOne("BackendPIA.Models.Raffle", "Raffle") + .WithMany("Prizes") + .HasForeignKey("RaffleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Raffle"); + }); + + modelBuilder.Entity("BackendPIA.Models.RaffleWinner", b => + { + b.HasOne("BackendPIA.Models.Prize", "Prize") + .WithMany() + .HasForeignKey("PrizeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BackendPIA.Models.Raffle", "Raffle") + .WithMany() + .HasForeignKey("RaffleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BackendPIA.Models.UserAccount", "UserAccount") + .WithMany() + .HasForeignKey("UserAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Prize"); + + b.Navigation("Raffle"); + + b.Navigation("UserAccount"); + }); + + modelBuilder.Entity("BackendPIA.Models.Ticket", b => + { + b.HasOne("BackendPIA.Models.Raffle", "Raffle") + .WithMany("Tickets") + .HasForeignKey("RaffleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("BackendPIA.Models.UserAccount", "Owner") + .WithMany("Tickets") + .HasForeignKey("UserAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Raffle"); + }); + + 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("BackendPIA.Models.UserAccount", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("BackendPIA.Models.UserAccount", 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("BackendPIA.Models.UserAccount", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("BackendPIA.Models.UserAccount", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("BackendPIA.Models.Raffle", b => + { + b.Navigation("Prizes"); + + b.Navigation("Tickets"); + }); + + modelBuilder.Entity("BackendPIA.Models.UserAccount", b => + { + b.Navigation("Tickets"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20221127165449_AddCurrentTokenToUser.cs b/Migrations/20221127165449_AddCurrentTokenToUser.cs new file mode 100644 index 0000000..cc998b2 --- /dev/null +++ b/Migrations/20221127165449_AddCurrentTokenToUser.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace BackendPIA.Migrations +{ + /// + public partial class AddCurrentTokenToUser : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CurrentToken", + table: "AspNetUsers", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CurrentToken", + table: "AspNetUsers"); + } + } +} diff --git a/Migrations/ApplicationDbContextModelSnapshot.cs b/Migrations/ApplicationDbContextModelSnapshot.cs index 917cafd..37dac6d 100644 --- a/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Migrations/ApplicationDbContextModelSnapshot.cs @@ -146,6 +146,9 @@ namespace BackendPIA.Migrations .IsConcurrencyToken() .HasColumnType("text"); + b.Property("CurrentToken") + .HasColumnType("text"); + b.Property("Email") .HasMaxLength(256) .HasColumnType("character varying(256)"); @@ -340,7 +343,7 @@ namespace BackendPIA.Migrations modelBuilder.Entity("BackendPIA.Models.Prize", b => { b.HasOne("BackendPIA.Models.Raffle", "Raffle") - .WithMany() + .WithMany("Prizes") .HasForeignKey("RaffleId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -447,6 +450,8 @@ namespace BackendPIA.Migrations modelBuilder.Entity("BackendPIA.Models.Raffle", b => { + b.Navigation("Prizes"); + b.Navigation("Tickets"); }); diff --git a/Models/UserAccount.cs b/Models/UserAccount.cs index 333138d..94ee69b 100644 --- a/Models/UserAccount.cs +++ b/Models/UserAccount.cs @@ -5,6 +5,7 @@ namespace BackendPIA.Models { public class UserAccount : IdentityUser { [StringLength(64)] public string? SessionToken { get; set; } + public string? CurrentToken { get; set; } public DateTime? SessionTokenExpiryTime { get; set; } public ICollection? Tickets { get; set; } } diff --git a/Policies/CorrectTokenHandler.cs b/Policies/CorrectTokenHandler.cs new file mode 100644 index 0000000..7663ec8 --- /dev/null +++ b/Policies/CorrectTokenHandler.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Authorization; +using BackendPIA.Models; + +namespace BackendPIA.Policies { + public class CorrectTokenHandler : AuthorizationHandler { + private readonly UserManager _manager; + + public CorrectTokenHandler(UserManager manager) { + _manager = manager; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CorrectTokenRequirement requirement) { + if(context.Resource is HttpContext httpContext) { + var user = _manager.FindByEmailAsync(context.User.Claims.Where(c => c.Type.Contains("email")).First().Value).Result; + + if(user != null) { + string token = httpContext.Request.Headers["Authorization"].ToString().Split(' ')[1]; + + if(user.CurrentToken != null && user.CurrentToken == token) + context.Succeed(requirement); + } + } + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Policies/CorrectTokenRequirement.cs b/Policies/CorrectTokenRequirement.cs new file mode 100644 index 0000000..d89615f --- /dev/null +++ b/Policies/CorrectTokenRequirement.cs @@ -0,0 +1,5 @@ +using Microsoft.AspNetCore.Authorization; + +namespace BackendPIA.Policies { + public class CorrectTokenRequirement : IAuthorizationRequirement{} +} \ No newline at end of file diff --git a/Program.cs b/Program.cs index ed98bd4..793aa1e 100644 --- a/Program.cs +++ b/Program.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; @@ -8,6 +9,7 @@ using System.Text; using Microsoft.EntityFrameworkCore; using BackendPIA; using BackendPIA.Models; +using BackendPIA.Policies; using BackendPIA.Services; var builder = WebApplication.CreateBuilder(args); @@ -32,6 +34,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // End of custom services configuration. +// Register the custom authorization handler. +builder.Services.AddScoped(); // Swagger configuration. builder.Services.AddEndpointsApiExplorer(); @@ -75,6 +79,10 @@ builder.Services.AddAuthentication(options => { IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])), ClockSkew = TimeSpan.Zero } ); + +builder.Services.AddAuthorization(options => { + options.AddPolicy("ValidToken", policy => policy.Requirements.Add(new CorrectTokenRequirement())); +}); // End of authentication configuration. // Identity configuration. -- cgit v1.2.3