diff --git a/Server/Controllers/AuthenticationController.cs b/Server/Controllers/AuthenticationController.cs index 48a5260..acf21c4 100644 --- a/Server/Controllers/AuthenticationController.cs +++ b/Server/Controllers/AuthenticationController.cs @@ -1,18 +1,23 @@ +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; using Server.Services; +using Server.Settings; using SharedModels.Requests; namespace Server.Controllers; -[Route("api/[controller]")] +[Route("api/authentication")] [ApiController] public class AuthenticationController : ControllerBase { private readonly IAuthenticationService _authService; + private readonly Jwt _jwt; - public AuthenticationController(IAuthenticationService authService) + public AuthenticationController(IAuthenticationService authService, IOptions jwt) { _authService = authService; + _jwt = jwt.Value; } [HttpPost("register")] @@ -28,16 +33,67 @@ public class AuthenticationController : ControllerBase return Ok(result); } - [HttpPost("token")] + [HttpPost("authenticate")] public async Task GetTokenAsync(AuthenticationRequest authRequest) { - var authResponse = await _authService.GetTokenAsync(authRequest); + var authResponse = await _authService.AuthenticateAsync(authRequest); if (!authResponse.IsAuthenticated) { return BadRequest(authResponse); } + SetRefreshTokenInCookie(authResponse.RefreshToken); + return Ok(authResponse); } + + [HttpPost("renew-session")] + public async Task RenewTokens() + { + var refreshToken = Request.Cookies["refreshToken"]; + + var authResponse = await _authService.RenewRefreshTokenAsync(refreshToken); + + if (!authResponse.IsAuthenticated) + { + return BadRequest(authResponse); + } + + if (!String.IsNullOrEmpty(authResponse.RefreshToken)) + { + SetRefreshTokenInCookie(authResponse.RefreshToken); + } + + return Ok(authResponse); + } + + [HttpPost("revoke-session")] + public async Task RevokeToken([FromBody] RevokeRefreshTokenRequest revokeRequest) + { + // accept token from request body or cookie + var token = revokeRequest?.Token ?? Request.Cookies["refreshToken"]; + if (string.IsNullOrEmpty(token)) + { + return BadRequest(new { message = "Refresh token is required." }); + } + + var response = await _authService.RevokeRefreshToken(token); + if (!response) + { + return NotFound(new { message = "Refresh token not found." }); + } + + return Ok(new { message = "Refresh token revoked." }); + } + + private void SetRefreshTokenInCookie(string refreshToken) + { + var cookieOptions = new CookieOptions + { + HttpOnly = true, + Expires = DateTime.UtcNow.AddDays(_jwt.RefreshTokenValidityInDays) + }; + Response.Cookies.Append("refreshToken", refreshToken, cookieOptions); + } } \ No newline at end of file diff --git a/Server/Controllers/SecuredController.cs b/Server/Controllers/SecuredController.cs index ce1dc15..fb1623c 100644 --- a/Server/Controllers/SecuredController.cs +++ b/Server/Controllers/SecuredController.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Mvc; namespace Server.Controllers; [Authorize(Roles = "Admin")] -[Route("api/[controller]")] +[Route("api/secured")] [ApiController] public class SecuredController : ControllerBase { diff --git a/Server/Entities/RefreshToken.cs b/Server/Entities/RefreshToken.cs new file mode 100644 index 0000000..b261401 --- /dev/null +++ b/Server/Entities/RefreshToken.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; + +namespace Server.Entities; + +[Owned] +public class RefreshToken +{ + public string Token { get; set; } = null!; + public DateTime Expires { get; set; } + public bool IsExpired => DateTime.UtcNow >= Expires; + public DateTime Created { get; set; } + public DateTime? Revoked { get; set; } + public bool IsActive => Revoked == null && !IsExpired; +} \ No newline at end of file diff --git a/Server/Migrations/20220902061345_Add refresh tokens.Designer.cs b/Server/Migrations/20220902061345_Add refresh tokens.Designer.cs new file mode 100644 index 0000000..1df31a6 --- /dev/null +++ b/Server/Migrations/20220902061345_Add refresh tokens.Designer.cs @@ -0,0 +1,319 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Server.Data; + +#nullable disable + +namespace Server.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20220902061345_Add refresh tokens")] + partial class Addrefreshtokens + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + 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("Server.Models.ApplicationUser", 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("FirstName") + .HasColumnType("text"); + + b.Property("LastName") + .HasColumnType("text"); + + 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("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("Server.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Server.Models.ApplicationUser", 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("Server.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Server.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Server.Models.ApplicationUser", b => + { + b.OwnsMany("Server.Entities.RefreshToken", "RefreshTokens", b1 => + { + b1.Property("ApplicationUserId") + .HasColumnType("text"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Created") + .HasColumnType("timestamp with time zone"); + + b1.Property("Expires") + .HasColumnType("timestamp with time zone"); + + b1.Property("Revoked") + .HasColumnType("timestamp with time zone"); + + b1.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("ApplicationUserId", "Id"); + + b1.ToTable("RefreshToken"); + + b1.WithOwner() + .HasForeignKey("ApplicationUserId"); + }); + + b.Navigation("RefreshTokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20220902061345_Add refresh tokens.cs b/Server/Migrations/20220902061345_Add refresh tokens.cs new file mode 100644 index 0000000..25bfbbf --- /dev/null +++ b/Server/Migrations/20220902061345_Add refresh tokens.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Server.Migrations +{ + public partial class Addrefreshtokens : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "RefreshToken", + columns: table => new + { + ApplicationUserId = table.Column(type: "text", nullable: false), + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Token = table.Column(type: "text", nullable: false), + Expires = table.Column(type: "timestamp with time zone", nullable: false), + Created = table.Column(type: "timestamp with time zone", nullable: false), + Revoked = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RefreshToken", x => new { x.ApplicationUserId, x.Id }); + table.ForeignKey( + name: "FK_RefreshToken_AspNetUsers_ApplicationUserId", + column: x => x.ApplicationUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RefreshToken"); + } + } +} diff --git a/Server/Migrations/ApplicationDbContextModelSnapshot.cs b/Server/Migrations/ApplicationDbContextModelSnapshot.cs index cf2f569..b64ef52 100644 --- a/Server/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Server/Migrations/ApplicationDbContextModelSnapshot.cs @@ -274,6 +274,43 @@ namespace Server.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); + + modelBuilder.Entity("Server.Models.ApplicationUser", b => + { + b.OwnsMany("Server.Entities.RefreshToken", "RefreshTokens", b1 => + { + b1.Property("ApplicationUserId") + .HasColumnType("text"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Created") + .HasColumnType("timestamp with time zone"); + + b1.Property("Expires") + .HasColumnType("timestamp with time zone"); + + b1.Property("Revoked") + .HasColumnType("timestamp with time zone"); + + b1.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("ApplicationUserId", "Id"); + + b1.ToTable("RefreshToken"); + + b1.WithOwner() + .HasForeignKey("ApplicationUserId"); + }); + + b.Navigation("RefreshTokens"); + }); #pragma warning restore 612, 618 } } diff --git a/Server/Models/ApplicationUser.cs b/Server/Models/ApplicationUser.cs index 2cfd23b..8c86e56 100644 --- a/Server/Models/ApplicationUser.cs +++ b/Server/Models/ApplicationUser.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Identity; +using Server.Entities; namespace Server.Models; @@ -6,4 +7,5 @@ public class ApplicationUser : IdentityUser { public string? FirstName { get; set; } public string? LastName { get; set; } + public List RefreshTokens { get; set; } = null!; } \ No newline at end of file diff --git a/Server/Program.cs b/Server/Program.cs index 04ed1d3..d3489d2 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -87,10 +87,10 @@ if (app.Environment.IsDevelopment()) app.UseHttpsRedirection(); // Data seeding -using var scope = app.Services.CreateScope(); -var userManager = (UserManager)scope.ServiceProvider.GetService(typeof(UserManager))!; -var roleManager = (RoleManager)scope.ServiceProvider.GetService(typeof(RoleManager))!; -await ApplicationDbContextSeed.SeedEssentialsAsync(userManager, roleManager); +// using var scope = app.Services.CreateScope(); +// var userManager = (UserManager)scope.ServiceProvider.GetService(typeof(UserManager))!; +// var roleManager = (RoleManager)scope.ServiceProvider.GetService(typeof(RoleManager))!; +// await ApplicationDbContextSeed.SeedEssentialsAsync(userManager, roleManager); app.MapControllers(); diff --git a/Server/Services/AuthenticationService.cs b/Server/Services/AuthenticationService.cs index 5ec6a6c..e2bbcb0 100644 --- a/Server/Services/AuthenticationService.cs +++ b/Server/Services/AuthenticationService.cs @@ -1,10 +1,13 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; +using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using Server.Constants; +using Server.Entities; using Server.Models; using Server.Settings; using SharedModels.Requests; @@ -18,7 +21,8 @@ public class AuthenticationService : IAuthenticationService private readonly RoleManager _roleManager; private readonly Jwt _jwt; - public AuthenticationService(UserManager userManager, RoleManager roleManager, IOptions jwt) + public AuthenticationService(UserManager userManager, + RoleManager roleManager, IOptions jwt) { _userManager = userManager; _roleManager = roleManager; @@ -47,7 +51,7 @@ public class AuthenticationService : IAuthenticationService } } - public async Task GetTokenAsync(AuthenticationRequest authRequest) + public async Task AuthenticateAsync(AuthenticationRequest authRequest) { var authResponse = new AuthenticationResponse(); @@ -75,8 +79,95 @@ public class AuthenticationService : IAuthenticationService var jwtSecurityToken = await CreateJwtToken(user); authResponse.Token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); + if (user.RefreshTokens.Any(t => t.IsActive)) + { + var activeRefreshToken = + user.RefreshTokens.First(t => t.IsActive); + authResponse.RefreshToken = activeRefreshToken.Token; + authResponse.RefreshTokenExpiration = activeRefreshToken.Expires; + } + else + { + var refreshToken = CreateRefreshToken(); + authResponse.RefreshToken = refreshToken.Token; + authResponse.RefreshTokenExpiration = refreshToken.Expires; + user.RefreshTokens.Add(refreshToken); + await _userManager.UpdateAsync(user); + } + return authResponse; } + + public async Task RenewRefreshTokenAsync(string? token) + { + var authResponse = new AuthenticationResponse(); + + var user = await _userManager.Users.SingleOrDefaultAsync(u => + u.RefreshTokens.Any(t => t.Token == token)); + + if (user == null) + { + authResponse.IsAuthenticated = false; + authResponse.Message = "Refresh token did not mach any user."; + return authResponse; + } + + var refreshToken = user.RefreshTokens.Single(t => t.Token == token); + + if (!refreshToken.IsActive) + { + authResponse.IsAuthenticated = false; + authResponse.Message = "Refresh token expired."; + return authResponse; + } + + //Revoke Current Refresh Token + refreshToken.Revoked = DateTime.UtcNow; + + //Generate new Refresh Token and save to Database + var newRefreshToken = CreateRefreshToken(); + user.RefreshTokens.Add(newRefreshToken); + await _userManager.UpdateAsync(user); + + //Generates new jwt + authResponse.IsAuthenticated = true; + authResponse.Email = user.Email; + authResponse.UserName = user.UserName; + + var roles = await _userManager.GetRolesAsync(user); + authResponse.Roles = roles.ToList(); + + var jwtSecurityToken = await CreateJwtToken(user); + authResponse.Token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); + + authResponse.RefreshToken = newRefreshToken.Token; + authResponse.RefreshTokenExpiration = newRefreshToken.Expires; + + return authResponse; + } + + public async Task RevokeRefreshToken(string? token) + { + var user = await _userManager.Users.SingleOrDefaultAsync(u => + u.RefreshTokens.Any(t => t.Token == token)); + + if (user == null) + { + return false; + } + + var refreshToken = user.RefreshTokens.Single(x => x.Token == token); + + if (!refreshToken.IsActive) + { + return false; + } + + refreshToken.Revoked = DateTime.UtcNow; + await _userManager.UpdateAsync(user); + + return true; + } private async Task CreateJwtToken(ApplicationUser user) { @@ -112,4 +203,19 @@ public class AuthenticationService : IAuthenticationService return jwtSecurityToken; } + + private RefreshToken CreateRefreshToken() + { + var randomNumber = new byte[32]; + + using var rng = RandomNumberGenerator.Create(); + rng.GetNonZeroBytes(randomNumber); + + return new RefreshToken + { + Token = Convert.ToBase64String(randomNumber), + Created = DateTime.UtcNow, + Expires = DateTime.UtcNow.AddDays(_jwt.RefreshTokenValidityInDays) + }; + } } \ No newline at end of file diff --git a/Server/Services/IAuthenticationService.cs b/Server/Services/IAuthenticationService.cs index c8eaee0..b701876 100644 --- a/Server/Services/IAuthenticationService.cs +++ b/Server/Services/IAuthenticationService.cs @@ -8,5 +8,9 @@ public interface IAuthenticationService { Task<(bool succeeded, string message)> RegisterAsync(RegistrationRequest regRequest); - Task GetTokenAsync(AuthenticationRequest authRequest); + Task AuthenticateAsync(AuthenticationRequest authRequest); + + Task RenewRefreshTokenAsync(string? token); + + Task RevokeRefreshToken(string? token); } \ No newline at end of file diff --git a/Server/Settings/Jwt.cs b/Server/Settings/Jwt.cs index 6b659d6..d22de73 100644 --- a/Server/Settings/Jwt.cs +++ b/Server/Settings/Jwt.cs @@ -6,4 +6,5 @@ public class Jwt public string Audience { get; set; } = null!; public string Issuer { get; set; } = null!; public double ValidityInMinutes { get; set; } + public double RefreshTokenValidityInDays { get; set; } } \ No newline at end of file diff --git a/Server/appsettings.Development.json b/Server/appsettings.Development.json index 4874308..cf2d2fa 100644 --- a/Server/appsettings.Development.json +++ b/Server/appsettings.Development.json @@ -12,6 +12,7 @@ "Key": "Secret which will never be exposed", "Audience": "Application URL", "Issuer": "Application URL", - "ValidityInMinutes": 1 + "ValidityInMinutes": 1, + "RefreshTokenValidityInDays": 10 } } diff --git a/Server/appsettings.json b/Server/appsettings.json index 23d0f45..c4e1c0a 100644 --- a/Server/appsettings.json +++ b/Server/appsettings.json @@ -13,6 +13,7 @@ "Key": "Secret which will never be exposed", "Audience": "Application URL", "Issuer": "Application URL", - "ValidityInMinutes": 1 + "ValidityInMinutes": 1, + "RefreshTokenValidityInDays": 10 } } diff --git a/SharedModels/Requests/RevokeRefreshTokenRequest.cs b/SharedModels/Requests/RevokeRefreshTokenRequest.cs new file mode 100644 index 0000000..a09d0b9 --- /dev/null +++ b/SharedModels/Requests/RevokeRefreshTokenRequest.cs @@ -0,0 +1,6 @@ +namespace SharedModels.Requests; + +public class RevokeRefreshTokenRequest +{ + public string? Token { get; set; } +} \ No newline at end of file diff --git a/SharedModels/Responses/AuthenticationResponse.cs b/SharedModels/Responses/AuthenticationResponse.cs index 4888fee..7b67e9d 100644 --- a/SharedModels/Responses/AuthenticationResponse.cs +++ b/SharedModels/Responses/AuthenticationResponse.cs @@ -10,4 +10,7 @@ public class AuthenticationResponse public string Email { get; set; } = null!; public List Roles { get; set; } = null!; public string Token { get; set; } = null!; + [JsonIgnore] + public string RefreshToken { get; set; } = null!; + public DateTime RefreshTokenExpiration { get; set; } } \ No newline at end of file