diff options
-rw-r--r-- | Controllers/RafflesController.cs | 2 | ||||
-rw-r--r-- | Controllers/UserAccountSessionsController.cs | 15 | ||||
-rw-r--r-- | Logics/BaseUserAccountLogic.cs | 2 | ||||
-rw-r--r-- | Logics/DestroyUserAccountSessionLogic.cs | 30 | ||||
-rw-r--r-- | Logics/RefreshTokenLogic.cs | 1 | ||||
-rw-r--r-- | Migrations/20221127165449_AddCurrentTokenToUser.Designer.cs | 468 | ||||
-rw-r--r-- | Migrations/20221127165449_AddCurrentTokenToUser.cs | 28 | ||||
-rw-r--r-- | Migrations/ApplicationDbContextModelSnapshot.cs | 7 | ||||
-rw-r--r-- | Models/UserAccount.cs | 1 | ||||
-rw-r--r-- | Policies/CorrectTokenHandler.cs | 29 | ||||
-rw-r--r-- | Policies/CorrectTokenRequirement.cs | 5 | ||||
-rw-r--r-- | Program.cs | 8 |
12 files changed, 593 insertions, 3 deletions
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<ActionResult<IEnumerable<int>>> AvailableTickets(long id) { IEnumerable<int> 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<ActionResult> 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<ActionResult<AuthenticationToken>> 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<UserAccount> _manager; + private readonly string _email; + + public DestroyUserAccountSessionLogic(UserManager<UserAccount> manager, string email) { + _manager = manager; + _email = email; + } + + public async Task<bool> 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 @@ +// <auto-generated /> +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 + { + /// <inheritdoc /> + 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<long>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); + + b.Property<string>("Category") + .IsRequired() + .HasColumnType("text"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property<long>("RaffleId") + .HasColumnType("bigint"); + + b.Property<int>("Tier") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RaffleId"); + + b.ToTable("Prizes"); + }); + + modelBuilder.Entity("BackendPIA.Models.Raffle", b => + { + b.Property<long>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); + + b.Property<bool>("IsClosed") + .HasColumnType("boolean"); + + b.Property<string>("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property<int>("Winners") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Raffles"); + }); + + modelBuilder.Entity("BackendPIA.Models.RaffleWinner", b => + { + b.Property<long>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); + + b.Property<long>("PrizeId") + .HasColumnType("bigint"); + + b.Property<long>("RaffleId") + .HasColumnType("bigint"); + + b.Property<string>("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<long>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); + + b.Property<bool>("IsWinner") + .HasColumnType("boolean"); + + b.Property<int>("Number") + .HasColumnType("integer"); + + b.Property<long>("RaffleId") + .HasColumnType("bigint"); + + b.Property<string>("UserAccountId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RaffleId"); + + b.HasIndex("UserAccountId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("BackendPIA.Models.UserAccount", b => + { + b.Property<string>("Id") + .HasColumnType("text"); + + b.Property<int>("AccessFailedCount") + .HasColumnType("integer"); + + b.Property<string>("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property<string>("CurrentToken") + .HasColumnType("text"); + + b.Property<string>("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<bool>("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property<bool>("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property<DateTimeOffset?>("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<string>("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<string>("PasswordHash") + .HasColumnType("text"); + + b.Property<string>("PhoneNumber") + .HasColumnType("text"); + + b.Property<bool>("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property<string>("SecurityStamp") + .HasColumnType("text"); + + b.Property<string>("SessionToken") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property<DateTime?>("SessionTokenExpiryTime") + .HasColumnType("timestamp with time zone"); + + b.Property<bool>("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property<string>("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<string>("Id") + .HasColumnType("text"); + + b.Property<string>("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property<string>("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<string>("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<string>", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("ClaimType") + .HasColumnType("text"); + + b.Property<string>("ClaimValue") + .HasColumnType("text"); + + b.Property<string>("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("ClaimType") + .HasColumnType("text"); + + b.Property<string>("ClaimValue") + .HasColumnType("text"); + + b.Property<string>("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b => + { + b.Property<string>("LoginProvider") + .HasColumnType("text"); + + b.Property<string>("ProviderKey") + .HasColumnType("text"); + + b.Property<string>("ProviderDisplayName") + .HasColumnType("text"); + + b.Property<string>("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b => + { + b.Property<string>("UserId") + .HasColumnType("text"); + + b.Property<string>("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b => + { + b.Property<string>("UserId") + .HasColumnType("text"); + + b.Property<string>("LoginProvider") + .HasColumnType("text"); + + b.Property<string>("Name") + .HasColumnType("text"); + + b.Property<string>("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<string>", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b => + { + b.HasOne("BackendPIA.Models.UserAccount", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b => + { + b.HasOne("BackendPIA.Models.UserAccount", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", 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<string>", 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 +{ + /// <inheritdoc /> + public partial class AddCurrentTokenToUser : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn<string>( + name: "CurrentToken", + table: "AspNetUsers", + type: "text", + nullable: true); + } + + /// <inheritdoc /> + 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<string>("CurrentToken") + .HasColumnType("text"); + b.Property<string>("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<Ticket>? 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<CorrectTokenRequirement> { + private readonly UserManager<UserAccount> _manager; + + public CorrectTokenHandler(UserManager<UserAccount> 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 @@ -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<IRaffleService, RaffleService>(); builder.Services.AddScoped<ITicketService, TicketService>(); builder.Services.AddScoped<IPrizeService, PrizeService>(); // End of custom services configuration. +// Register the custom authorization handler. +builder.Services.AddScoped<IAuthorizationHandler, CorrectTokenHandler>(); // 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. |