From 8ba6d5c324573aa56a94c1a586219d134da719eb Mon Sep 17 00:00:00 2001 From: Oscar Veldman Date: Sat, 3 Feb 2024 20:13:00 +0100 Subject: [PATCH] Add Audience on the refresh token --- MadWorld/MadWorld.Backend.API/Program.cs | 15 +- .../appsettings.Development.json | 21 +- .../MadWorld.Backend.API/appsettings.json | 19 +- .../UserManagers/RefreshTokenContract.cs | 1 + .../Application/JwtGenerator.cs | 4 +- .../Application/Mappers/IdentityUserMapper.cs | 1 + .../Application/PostJwtLoginUseCase.cs | 6 +- .../Application/PostJwtRefreshUseCase.cs | 4 +- .../Domain/IJwtGenerator.cs | 2 +- .../Domain/Users/RefreshToken.cs | 4 +- .../Endpoints/IdentityEndpoints.cs | 9 +- .../Extensions/RequestExtensions.cs | 13 + .../RefreshTokenEntityTypeConfiguration.cs | 4 + ...2949_AddAudienceInRefreshToken.Designer.cs | 324 ++++++++++++++++++ ...0240203182949_AddAudienceInRefreshToken.cs | 30 ++ .../Migrations/UserDbContextModelSnapshot.cs | 5 + MadWorld/MadWorld.Backend.Identity/Program.cs | 8 +- .../appsettings.Development.json | 21 +- .../appsettings.json | 19 +- .../DeleteExpiredSessionsUseCaseTests.cs | 4 +- .../Identity/DeleteSessionEndpointTests.cs | 6 +- .../Identity/GetUserEndpointTests.cs | 3 +- .../UserManagement/RefreshTokenDetails.cs | 1 + .../Application/UserManagement/UserMapper.cs | 1 + .../Layout/NavMenu.razor | 27 +- .../Pages/ShipSimulator/HardReset.razor | 3 + .../Pages/UserManagement/User.razor | 6 +- .../Pages/UserManagement/Users.razor | 5 +- .../Properties/launchSettings.json | 2 +- .../MadWorld.ShipSimulator.API/Program.cs | 8 +- .../appsettings.Development.json | 21 +- .../appsettings.json | 19 +- 32 files changed, 561 insertions(+), 55 deletions(-) create mode 100644 MadWorld/MadWorld.Backend.Identity/Extensions/RequestExtensions.cs create mode 100644 MadWorld/MadWorld.Backend.Identity/Migrations/20240203182949_AddAudienceInRefreshToken.Designer.cs create mode 100644 MadWorld/MadWorld.Backend.Identity/Migrations/20240203182949_AddAudienceInRefreshToken.cs diff --git a/MadWorld/MadWorld.Backend.API/Program.cs b/MadWorld/MadWorld.Backend.API/Program.cs index a84cac0..6c3d635 100644 --- a/MadWorld/MadWorld.Backend.API/Program.cs +++ b/MadWorld/MadWorld.Backend.API/Program.cs @@ -58,7 +58,7 @@ private static void Main(string[] args) ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], - ValidAudience = builder.Configuration["Jwt:Audience"], + ValidAudiences = builder.Configuration.GetSection("Jwt:Audiences").Get(), IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)) }; @@ -75,6 +75,18 @@ private static void Main(string[] args) options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; }); + + const string madWorldOrigins = "_myAllowSpecificOrigins"; + builder.Services.AddCors(options => + { + options.AddPolicy(name: madWorldOrigins, + policy => + { + policy.WithOrigins(builder.Configuration.GetSection("Cors:AllowedOrigins").Get()!); + policy.AllowAnyMethod(); + policy.AllowAnyHeader(); + }); + }); builder.Services.AddRateLimiter(rateLimiterOptions => { @@ -107,6 +119,7 @@ private static void Main(string[] args) app.AddTestEndpoints(); app.UseRateLimiter(); + app.UseCors(madWorldOrigins); app.MigrateDatabases(); diff --git a/MadWorld/MadWorld.Backend.API/appsettings.Development.json b/MadWorld/MadWorld.Backend.API/appsettings.Development.json index 723e029..1e19f03 100644 --- a/MadWorld/MadWorld.Backend.API/appsettings.Development.json +++ b/MadWorld/MadWorld.Backend.API/appsettings.Development.json @@ -5,8 +5,25 @@ }, "Jwt": { "Key": "mSWX4ctFHyPAPYddRzgVETAUEj3oJE2cNCPfhbyW9K5M4rXYjR", - "Issuer": "https://identity.mad-world.nl/", - "Audience": "https://identity.mad-world.nl/" + "Issuer": "https://localhost:7056/", + "Audiences": [ + "https://localhost:7298/", + "https://localhost:7177/", + "https://localhost:7056/", + "https://localhost:7180/", + "https://localhost:7205/", + "https://localhost:7300/" + ] + }, + "Cors": { + "AllowedOrigins": [ + "https://localhost:7298", + "https://localhost:7177", + "https://localhost:7056", + "https://localhost:7180", + "https://localhost:7205", + "https://localhost:7300" + ] }, "Logging": { "LogLevel": { diff --git a/MadWorld/MadWorld.Backend.API/appsettings.json b/MadWorld/MadWorld.Backend.API/appsettings.json index 27785d8..d9a96f5 100644 --- a/MadWorld/MadWorld.Backend.API/appsettings.json +++ b/MadWorld/MadWorld.Backend.API/appsettings.json @@ -6,7 +6,24 @@ "Jwt": { "Key": "Empty", "Issuer": "https://identity.mad-world.nl/", - "Audience": "https://api.mad-world.nl/" + "Audiences": [ + "https://admin.mad-world.nl/", + "https://api.mad-world.nl/", + "https://identity.mad-world.nl/", + "https://shipsimulator.mad-world.nl/", + "https://shipsimulator-api.mad-world.nl/", + "https://www.mad-world.nl/" + ] + }, + "Cors": { + "AllowedOrigins": [ + "https://admin.mad-world.nl", + "https://api.mad-world.nl", + "https://identity.mad-world.nl", + "https://shipsimulator.mad-world.nl", + "https://shipsimulator-api.mad-world.nl", + "https://www.mad-world.nl" + ] }, "Logging": { "LogLevel": { diff --git a/MadWorld/MadWorld.Backend.Identity.Contracts/UserManagers/RefreshTokenContract.cs b/MadWorld/MadWorld.Backend.Identity.Contracts/UserManagers/RefreshTokenContract.cs index 4a6ba44..7d6405d 100644 --- a/MadWorld/MadWorld.Backend.Identity.Contracts/UserManagers/RefreshTokenContract.cs +++ b/MadWorld/MadWorld.Backend.Identity.Contracts/UserManagers/RefreshTokenContract.cs @@ -3,5 +3,6 @@ namespace MadWorld.Backend.Identity.Contracts.UserManagers; public class RefreshTokenContract { public string Id { get; set; } + public string Audience { get; set; } public DateTime Expires { get; set; } } \ No newline at end of file diff --git a/MadWorld/MadWorld.Backend.Identity/Application/JwtGenerator.cs b/MadWorld/MadWorld.Backend.Identity/Application/JwtGenerator.cs index 4940d5d..2fa0708 100644 --- a/MadWorld/MadWorld.Backend.Identity/Application/JwtGenerator.cs +++ b/MadWorld/MadWorld.Backend.Identity/Application/JwtGenerator.cs @@ -16,7 +16,7 @@ public JwtGenerator(IConfiguration configuration) _configuration = configuration; } - public JwtToken GenerateToken(IdentityUserExtended user, IList roles) + public JwtToken GenerateToken(IdentityUserExtended user, string audience, IList roles) { var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Key"]!); @@ -42,7 +42,7 @@ public JwtToken GenerateToken(IdentityUserExtended user, IList roles) Subject = new ClaimsIdentity(claims), Expires = expires, Issuer = _configuration["Jwt:Issuer"], - Audience = _configuration["Jwt:Audience"], + Audience = audience, SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) }; diff --git a/MadWorld/MadWorld.Backend.Identity/Application/Mappers/IdentityUserMapper.cs b/MadWorld/MadWorld.Backend.Identity/Application/Mappers/IdentityUserMapper.cs index 681820c..9e04dc6 100644 --- a/MadWorld/MadWorld.Backend.Identity/Application/Mappers/IdentityUserMapper.cs +++ b/MadWorld/MadWorld.Backend.Identity/Application/Mappers/IdentityUserMapper.cs @@ -31,6 +31,7 @@ private static RefreshTokenContract ToRefreshTokenContract(RefreshToken refreshT return new RefreshTokenContract() { Id = refreshToken.Id.ToString(), + Audience = refreshToken.Audience, Expires = refreshToken.Expires }; } diff --git a/MadWorld/MadWorld.Backend.Identity/Application/PostJwtLoginUseCase.cs b/MadWorld/MadWorld.Backend.Identity/Application/PostJwtLoginUseCase.cs index 7c70ef9..cbb0d89 100644 --- a/MadWorld/MadWorld.Backend.Identity/Application/PostJwtLoginUseCase.cs +++ b/MadWorld/MadWorld.Backend.Identity/Application/PostJwtLoginUseCase.cs @@ -22,7 +22,7 @@ public PostJwtLoginUseCase(IJwtGenerator jwtGenerator, IUserRepository userRepos _userManager = userManager; } - public async Task PostJwtLogin(JwtLoginRequest request) + public async Task PostJwtLogin(JwtLoginRequest request, string audience) { var result = await _signInManager.PasswordSignInAsync(request.Email, request.Password, false, false); if (!result.Succeeded) @@ -33,10 +33,10 @@ public async Task PostJwtLogin(JwtLoginRequest request) var user = await _userManager.FindByEmailAsync(request.Email); var roles = await _userManager.GetRolesAsync(user!); - var jwt = _jwtGenerator.GenerateToken(user!, roles); + var jwt = _jwtGenerator.GenerateToken(user!, audience, roles); var token = GenerateRefreshToken(); - var refreshToken = new RefreshToken(token, expires: DateTime.UtcNow.AddDays(7), user!.Id); + var refreshToken = new RefreshToken(token, audience, expires: DateTime.UtcNow.AddDays(7), user!.Id); await _userRepository.AddRefreshToken(refreshToken); return Results.Ok(new JwtLoginResponse diff --git a/MadWorld/MadWorld.Backend.Identity/Application/PostJwtRefreshUseCase.cs b/MadWorld/MadWorld.Backend.Identity/Application/PostJwtRefreshUseCase.cs index 6d9d23f..f64520b 100644 --- a/MadWorld/MadWorld.Backend.Identity/Application/PostJwtRefreshUseCase.cs +++ b/MadWorld/MadWorld.Backend.Identity/Application/PostJwtRefreshUseCase.cs @@ -19,7 +19,7 @@ public PostJwtRefreshUseCase(IJwtGenerator jwtGenerator, UserManager PostJwtRefresh(JwtRefreshRequest request) + public async Task PostJwtRefresh(JwtRefreshRequest request, string audience) { var refreshToken = _userRepository.GetRefreshToken(request.RefreshToken); @@ -31,7 +31,7 @@ public async Task PostJwtRefresh(JwtRefreshRequest request) var user = refreshToken.User; var roles = await _userManager.GetRolesAsync(user); - var jwt = _jwtGenerator.GenerateToken(user, roles); + var jwt = _jwtGenerator.GenerateToken(user, audience, roles); return Results.Ok(new JwtRefreshResponse() { diff --git a/MadWorld/MadWorld.Backend.Identity/Domain/IJwtGenerator.cs b/MadWorld/MadWorld.Backend.Identity/Domain/IJwtGenerator.cs index 7b6a891..ad7262f 100644 --- a/MadWorld/MadWorld.Backend.Identity/Domain/IJwtGenerator.cs +++ b/MadWorld/MadWorld.Backend.Identity/Domain/IJwtGenerator.cs @@ -4,5 +4,5 @@ namespace MadWorld.Backend.Identity.Domain; public interface IJwtGenerator { - JwtToken GenerateToken(IdentityUserExtended user, IList roles); + JwtToken GenerateToken(IdentityUserExtended user, string audience, IList roles); } \ No newline at end of file diff --git a/MadWorld/MadWorld.Backend.Identity/Domain/Users/RefreshToken.cs b/MadWorld/MadWorld.Backend.Identity/Domain/Users/RefreshToken.cs index 843b223..9d86f56 100644 --- a/MadWorld/MadWorld.Backend.Identity/Domain/Users/RefreshToken.cs +++ b/MadWorld/MadWorld.Backend.Identity/Domain/Users/RefreshToken.cs @@ -6,15 +6,17 @@ public class RefreshToken { public const int MaxLength = 1000; - public RefreshToken(string token, DateTime expires, string userId) + public RefreshToken(string token, string audience, DateTime expires, string userId) { Token = token; + Audience = audience; Expires = expires; UserId = userId; } private RefreshToken() {} public Guid Id { get; set; } + public string Audience { get; set; } = string.Empty; public string Token { get; set; } = string.Empty; public DateTime Expires { get; set; } diff --git a/MadWorld/MadWorld.Backend.Identity/Endpoints/IdentityEndpoints.cs b/MadWorld/MadWorld.Backend.Identity/Endpoints/IdentityEndpoints.cs index 13da1ac..c3a3c73 100644 --- a/MadWorld/MadWorld.Backend.Identity/Endpoints/IdentityEndpoints.cs +++ b/MadWorld/MadWorld.Backend.Identity/Endpoints/IdentityEndpoints.cs @@ -5,6 +5,7 @@ using MadWorld.Backend.Identity.Application; using MadWorld.Backend.Identity.Contracts; using MadWorld.Backend.Identity.Domain.Users; +using MadWorld.Backend.Identity.Extensions; using MadWorld.Shared.Infrastructure.Settings; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; @@ -24,15 +25,15 @@ public static void AddIdentityEndpoints(this WebApplication app) .WithOpenApi(); account.MapPost("/JwtLogin", - ([FromBody] JwtLoginRequest request, [FromServices] PostJwtLoginUseCase useCase) => - useCase.PostJwtLogin(request)) + ([FromBody] JwtLoginRequest request, [FromServices] PostJwtLoginUseCase useCase, HttpContext context) => + useCase.PostJwtLogin(request, context.Request.GetBaseUrl())) .WithName("JwtLogin") .WithOpenApi() .AllowAnonymous(); account.MapPost("/JwtRefresh", - ([FromBody] JwtRefreshRequest request, [FromServices] PostJwtRefreshUseCase useCase) => - useCase.PostJwtRefresh(request)) + ([FromBody] JwtRefreshRequest request, [FromServices] PostJwtRefreshUseCase useCase, HttpContext context) => + useCase.PostJwtRefresh(request, context.Request.GetBaseUrl())) .WithName("JwtRefresh") .WithOpenApi() .AllowAnonymous(); diff --git a/MadWorld/MadWorld.Backend.Identity/Extensions/RequestExtensions.cs b/MadWorld/MadWorld.Backend.Identity/Extensions/RequestExtensions.cs new file mode 100644 index 0000000..8bc8aba --- /dev/null +++ b/MadWorld/MadWorld.Backend.Identity/Extensions/RequestExtensions.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.Primitives; + +namespace MadWorld.Backend.Identity.Extensions; + +public static class RequestExtensions +{ + public static string GetBaseUrl(this HttpRequest request) + { + request.Headers.TryGetValue("Origin", out var originValues); + var url = originValues.Count != 0 ? originValues[0]! : string.Empty; + return url.TrimEnd('/') + "/"; + } +} \ No newline at end of file diff --git a/MadWorld/MadWorld.Backend.Identity/Infrastructure/RefreshTokenEntityTypeConfiguration.cs b/MadWorld/MadWorld.Backend.Identity/Infrastructure/RefreshTokenEntityTypeConfiguration.cs index 22036d2..41b868b 100644 --- a/MadWorld/MadWorld.Backend.Identity/Infrastructure/RefreshTokenEntityTypeConfiguration.cs +++ b/MadWorld/MadWorld.Backend.Identity/Infrastructure/RefreshTokenEntityTypeConfiguration.cs @@ -10,6 +10,10 @@ public void Configure(EntityTypeBuilder builder) { builder.HasKey(x => x.Id); + builder.Property(x => x.Audience) + .IsRequired() + .HasMaxLength(RefreshToken.MaxLength); + builder.Property(x => x.Token) .IsRequired() .HasMaxLength(RefreshToken.MaxLength); diff --git a/MadWorld/MadWorld.Backend.Identity/Migrations/20240203182949_AddAudienceInRefreshToken.Designer.cs b/MadWorld/MadWorld.Backend.Identity/Migrations/20240203182949_AddAudienceInRefreshToken.Designer.cs new file mode 100644 index 0000000..3bc88a9 --- /dev/null +++ b/MadWorld/MadWorld.Backend.Identity/Migrations/20240203182949_AddAudienceInRefreshToken.Designer.cs @@ -0,0 +1,324 @@ +// +using System; +using MadWorld.Backend.Identity.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MadWorld.Backend.Identity.Migrations +{ + [DbContext(typeof(UserDbContext))] + [Migration("20240203182949_AddAudienceInRefreshToken")] + partial class AddAudienceInRefreshToken + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MadWorld.Backend.Identity.Domain.Users.IdentityUserExtended", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .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("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("MadWorld.Backend.Identity.Domain.Users.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Audience") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Expires") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + 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("MadWorld.Backend.Identity.Domain.Users.RefreshToken", b => + { + b.HasOne("MadWorld.Backend.Identity.Domain.Users.IdentityUserExtended", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + 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("MadWorld.Backend.Identity.Domain.Users.IdentityUserExtended", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("MadWorld.Backend.Identity.Domain.Users.IdentityUserExtended", 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("MadWorld.Backend.Identity.Domain.Users.IdentityUserExtended", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("MadWorld.Backend.Identity.Domain.Users.IdentityUserExtended", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MadWorld.Backend.Identity.Domain.Users.IdentityUserExtended", b => + { + b.Navigation("RefreshTokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MadWorld/MadWorld.Backend.Identity/Migrations/20240203182949_AddAudienceInRefreshToken.cs b/MadWorld/MadWorld.Backend.Identity/Migrations/20240203182949_AddAudienceInRefreshToken.cs new file mode 100644 index 0000000..0d8891d --- /dev/null +++ b/MadWorld/MadWorld.Backend.Identity/Migrations/20240203182949_AddAudienceInRefreshToken.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MadWorld.Backend.Identity.Migrations +{ + /// + public partial class AddAudienceInRefreshToken : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Audience", + table: "RefreshTokens", + type: "character varying(1000)", + maxLength: 1000, + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Audience", + table: "RefreshTokens"); + } + } +} diff --git a/MadWorld/MadWorld.Backend.Identity/Migrations/UserDbContextModelSnapshot.cs b/MadWorld/MadWorld.Backend.Identity/Migrations/UserDbContextModelSnapshot.cs index 7844b79..3df657a 100644 --- a/MadWorld/MadWorld.Backend.Identity/Migrations/UserDbContextModelSnapshot.cs +++ b/MadWorld/MadWorld.Backend.Identity/Migrations/UserDbContextModelSnapshot.cs @@ -92,6 +92,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("Audience") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + b.Property("Expires") .HasColumnType("timestamp with time zone"); diff --git a/MadWorld/MadWorld.Backend.Identity/Program.cs b/MadWorld/MadWorld.Backend.Identity/Program.cs index 71c0649..38c0e70 100644 --- a/MadWorld/MadWorld.Backend.Identity/Program.cs +++ b/MadWorld/MadWorld.Backend.Identity/Program.cs @@ -90,7 +90,7 @@ private static async Task Main(string[] args) ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], - ValidAudience = builder.Configuration["Jwt:Audience"], + ValidAudiences = builder.Configuration.GetSection("Jwt:Audiences").Get(), IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)) }; }); @@ -115,11 +115,7 @@ private static async Task Main(string[] args) options.AddPolicy(name: madWorldOrigins, policy => { - policy.WithOrigins( - "https://admin.mad-world.nl", - "https://shipsimulator.mad-world.nl", - "https://localhost:7298", - "https://localhost:7180"); + policy.WithOrigins(builder.Configuration.GetSection("Cors:AllowedOrigins").Get()!); policy.AllowAnyMethod(); policy.AllowAnyHeader(); }); diff --git a/MadWorld/MadWorld.Backend.Identity/appsettings.Development.json b/MadWorld/MadWorld.Backend.Identity/appsettings.Development.json index 7e7995d..941b5b3 100644 --- a/MadWorld/MadWorld.Backend.Identity/appsettings.Development.json +++ b/MadWorld/MadWorld.Backend.Identity/appsettings.Development.json @@ -5,8 +5,25 @@ }, "Jwt": { "Key": "mSWX4ctFHyPAPYddRzgVETAUEj3oJE2cNCPfhbyW9K5M4rXYjR", - "Issuer": "https://identity.mad-world.nl/", - "Audience": "https://identity.mad-world.nl/" + "Issuer": "https://localhost:7056/", + "Audiences": [ + "https://localhost:7298/", + "https://localhost:7177/", + "https://localhost:7056/", + "https://localhost:7180/", + "https://localhost:7205/", + "https://localhost:7300/" + ] + }, + "Cors": { + "AllowedOrigins": [ + "https://localhost:7298", + "https://localhost:7177", + "https://localhost:7056", + "https://localhost:7180", + "https://localhost:7205", + "https://localhost:7300" + ] }, "Logging": { "LogLevel": { diff --git a/MadWorld/MadWorld.Backend.Identity/appsettings.json b/MadWorld/MadWorld.Backend.Identity/appsettings.json index dae4bc9..9b772e0 100644 --- a/MadWorld/MadWorld.Backend.Identity/appsettings.json +++ b/MadWorld/MadWorld.Backend.Identity/appsettings.json @@ -6,7 +6,24 @@ "Jwt": { "Key": "Empty", "Issuer": "https://identity.mad-world.nl/", - "Audience": "https://api.mad-world.nl/" + "Audiences": [ + "https://admin.mad-world.nl/", + "https://api.mad-world.nl/", + "https://identity.mad-world.nl/", + "https://shipsimulator.mad-world.nl/", + "https://shipsimulator-api.mad-world.nl/", + "https://www.mad-world.nl/" + ] + }, + "Cors": { + "AllowedOrigins": [ + "https://admin.mad-world.nl", + "https://api.mad-world.nl", + "https://identity.mad-world.nl", + "https://shipsimulator.mad-world.nl", + "https://shipsimulator-api.mad-world.nl", + "https://www.mad-world.nl" + ] }, "Logging": { "LogLevel": { diff --git a/MadWorld/MadWorld.Backend.IntegrationTests/Identity/DeleteExpiredSessionsUseCaseTests.cs b/MadWorld/MadWorld.Backend.IntegrationTests/Identity/DeleteExpiredSessionsUseCaseTests.cs index ab4f8a9..0134372 100644 --- a/MadWorld/MadWorld.Backend.IntegrationTests/Identity/DeleteExpiredSessionsUseCaseTests.cs +++ b/MadWorld/MadWorld.Backend.IntegrationTests/Identity/DeleteExpiredSessionsUseCaseTests.cs @@ -33,8 +33,8 @@ public async Task Execute_WhenRequestIsValid_ShouldReturnValidResponse() var context = scope.ServiceProvider.GetRequiredService(); var user = context.Users.First(); - await context.RefreshTokens.AddAsync(new RefreshToken("Test", new DateTime(2024, 01, 27, 0, 0,0,0, DateTimeKind.Utc), user.Id)); - await context.RefreshTokens.AddAsync(new RefreshToken("Test2", new DateTime(2024, 01, 29, 0, 0,0,0, DateTimeKind.Utc), user.Id)); + await context.RefreshTokens.AddAsync(new RefreshToken("Test", "https://test", new DateTime(2024, 01, 27, 0, 0,0,0, DateTimeKind.Utc), user.Id)); + await context.RefreshTokens.AddAsync(new RefreshToken("Test2", "https://test", new DateTime(2024, 01, 29, 0, 0,0,0, DateTimeKind.Utc), user.Id)); await context.SaveChangesAsync(); } diff --git a/MadWorld/MadWorld.Backend.IntegrationTests/Identity/DeleteSessionEndpointTests.cs b/MadWorld/MadWorld.Backend.IntegrationTests/Identity/DeleteSessionEndpointTests.cs index 373468d..4b71750 100644 --- a/MadWorld/MadWorld.Backend.IntegrationTests/Identity/DeleteSessionEndpointTests.cs +++ b/MadWorld/MadWorld.Backend.IntegrationTests/Identity/DeleteSessionEndpointTests.cs @@ -39,8 +39,8 @@ public async Task Execute_WhenRequestIsValid_ShouldReturnValidResponse() await context.Users.AddAsync(otherUser); - await context.RefreshTokens.AddAsync(new RefreshToken("Test", new DateTime(2024, 01, 27, 0, 0,0,0, DateTimeKind.Utc), adminUser.Id)); - await context.RefreshTokens.AddAsync(new RefreshToken("Test2", new DateTime(2024, 01, 29, 0, 0,0,0, DateTimeKind.Utc), otherUser.Id)); + await context.RefreshTokens.AddAsync(new RefreshToken("Test", "https://test", new DateTime(2024, 01, 27, 0, 0,0,0, DateTimeKind.Utc), adminUser.Id)); + await context.RefreshTokens.AddAsync(new RefreshToken("Test2", "https://test", new DateTime(2024, 01, 29, 0, 0,0,0, DateTimeKind.Utc), otherUser.Id)); await context.SaveChangesAsync(); } @@ -48,7 +48,7 @@ public async Task Execute_WhenRequestIsValid_ShouldReturnValidResponse() var client = Factory.CreateClient(); // Act - var response = await client.DeleteAsync("/UserManager/Session?userId=" + userId); + var response = await client.DeleteAsync("/UserManager/Sessions?userId=" + userId); // Assert response.StatusCode.ShouldBe(HttpStatusCode.OK); diff --git a/MadWorld/MadWorld.Backend.IntegrationTests/Identity/GetUserEndpointTests.cs b/MadWorld/MadWorld.Backend.IntegrationTests/Identity/GetUserEndpointTests.cs index 8baf15a..eded7ea 100644 --- a/MadWorld/MadWorld.Backend.IntegrationTests/Identity/GetUserEndpointTests.cs +++ b/MadWorld/MadWorld.Backend.IntegrationTests/Identity/GetUserEndpointTests.cs @@ -50,7 +50,7 @@ await context.UserRoles.AddAsync(new IdentityUserRole() } await context.RefreshTokens.AddAsync( - new RefreshToken("TestTestTest", DateTime.UtcNow, userId.ToString())); + new RefreshToken("TestTestTest", "https://test",DateTime.UtcNow, userId.ToString())); await context.SaveChangesAsync(); } @@ -71,5 +71,6 @@ await context.RefreshTokens.AddAsync( result.Roles.ShouldContain("IdentityAdministrator"); result.RefreshTokens.Count.ShouldBe(1); result.RefreshTokens[0].Id = "TestTestTest"; + result.RefreshTokens[0].Audience = "https://test"; } } \ No newline at end of file diff --git a/MadWorld/MadWorld.Frontend.Admin/Application/UserManagement/RefreshTokenDetails.cs b/MadWorld/MadWorld.Frontend.Admin/Application/UserManagement/RefreshTokenDetails.cs index 5122821..702ad64 100644 --- a/MadWorld/MadWorld.Frontend.Admin/Application/UserManagement/RefreshTokenDetails.cs +++ b/MadWorld/MadWorld.Frontend.Admin/Application/UserManagement/RefreshTokenDetails.cs @@ -3,5 +3,6 @@ namespace MadWorld.Frontend.Admin.Application.UserManagement; public class RefreshTokenDetails { public string Id { get; set; } + public string Audience { get; set; } public DateTime Expires { get; set; } } \ No newline at end of file diff --git a/MadWorld/MadWorld.Frontend.Admin/Application/UserManagement/UserMapper.cs b/MadWorld/MadWorld.Frontend.Admin/Application/UserManagement/UserMapper.cs index 7d02449..69a4610 100644 --- a/MadWorld/MadWorld.Frontend.Admin/Application/UserManagement/UserMapper.cs +++ b/MadWorld/MadWorld.Frontend.Admin/Application/UserManagement/UserMapper.cs @@ -44,6 +44,7 @@ private static RefreshTokenDetails ToDetails(this RefreshTokenContract contract) return new RefreshTokenDetails { Id = contract.Id, + Audience = contract.Audience, Expires = contract.Expires }; } diff --git a/MadWorld/MadWorld.Frontend.Admin/Layout/NavMenu.razor b/MadWorld/MadWorld.Frontend.Admin/Layout/NavMenu.razor index f4da3d3..2c23d6a 100644 --- a/MadWorld/MadWorld.Frontend.Admin/Layout/NavMenu.razor +++ b/MadWorld/MadWorld.Frontend.Admin/Layout/NavMenu.razor @@ -1,4 +1,5 @@ -