| @@ -4,10 +4,16 @@ | |||
| { | |||
| Task<ServiceResponseDTO<AuthenticateResponseDto>> Authenticate(AuthenticateRequestDto model); | |||
| Task<RefreshTokenResultDto> RefreshTokenAsync(RefreshTokenRequestDto model); | |||
| Task<IEnumerable<User?>> GetAll(); | |||
| Task<User?> GetById(int id); | |||
| Task CreateUser(CreateUserRequestDto model); | |||
| Task<RefreshToken?> GetRefreshTokenByUserId(int userId); | |||
| Task UpdateRefreshToken(RefreshToken refreshToken); | |||
| } | |||
| } | |||
| @@ -7,12 +7,14 @@ namespace Diligent.WebAPI.Business.Services | |||
| private readonly AuthorizationSettings _authSettings; | |||
| private readonly UserManager<User> _userManager; | |||
| private readonly IMapper _mapper; | |||
| private readonly DatabaseContext _databaseContext; | |||
| public UserService(IOptions<AuthorizationSettings> authSettings, UserManager<User> userManager, IMapper mapper) | |||
| public UserService(IOptions<AuthorizationSettings> authSettings, UserManager<User> userManager, IMapper mapper, DatabaseContext databaseContext) | |||
| { | |||
| _authSettings = authSettings.Value; | |||
| _userManager = userManager; | |||
| _mapper = mapper; | |||
| _databaseContext = databaseContext; | |||
| } | |||
| public async Task<IEnumerable<User?>> GetAll() => | |||
| @@ -75,7 +77,7 @@ namespace Diligent.WebAPI.Business.Services | |||
| } | |||
| // authentication successful so generate jwt token | |||
| var token = GenerateJwtToken(user); | |||
| var token = await GenerateJwtToken(user, true); | |||
| var data = new AuthenticateResponseDto | |||
| { | |||
| @@ -83,7 +85,8 @@ namespace Diligent.WebAPI.Business.Services | |||
| Username = user.UserName, | |||
| FirstName = user.FirstName, | |||
| LastName = user.LastName, | |||
| Token = token | |||
| Token = token, | |||
| RefreshToken = token | |||
| }; | |||
| return new ServiceResponseDTO<AuthenticateResponseDto> | |||
| @@ -92,19 +95,174 @@ namespace Diligent.WebAPI.Business.Services | |||
| }; | |||
| } | |||
| private string GenerateJwtToken(User user) | |||
| private async Task<string> GenerateJwtToken(User user, bool authenticate = false) | |||
| { | |||
| // generate token that is valid for 7 days | |||
| var tokenHandler = new JwtSecurityTokenHandler(); | |||
| var key = Encoding.ASCII.GetBytes(_authSettings.Secret); | |||
| var tokenDescriptor = new SecurityTokenDescriptor | |||
| { | |||
| Subject = new ClaimsIdentity(new[] { new Claim("id", user.Id.ToString()) }), | |||
| Expires = DateTime.UtcNow.AddMinutes(2), | |||
| Subject = new ClaimsIdentity(new[] { | |||
| new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), | |||
| new Claim("id", user.Id.ToString()) | |||
| }), | |||
| Expires = DateTime.UtcNow.AddMinutes(_authSettings.JwtExpiredTime), | |||
| SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) | |||
| }; | |||
| var token = tokenHandler.CreateToken(tokenDescriptor); | |||
| return tokenHandler.WriteToken(token); | |||
| var writedToken = tokenHandler.WriteToken(token); | |||
| var refreshToken = new RefreshToken | |||
| { | |||
| Token = writedToken, | |||
| JwtId = token.Id, | |||
| UserId = user.Id, | |||
| User = user, | |||
| CreationDate = DateTime.UtcNow, | |||
| ExpiryDate = DateTime.UtcNow.AddMinutes(_authSettings.JwtRefreshExpiredTime) | |||
| }; | |||
| var existRefreshToken = await _databaseContext.RefreshTokens.Where(x => x.Id == user.Id).FirstOrDefaultAsync(); | |||
| if(existRefreshToken != null) | |||
| { | |||
| existRefreshToken.Token = writedToken; | |||
| existRefreshToken.JwtId = token.Id; | |||
| existRefreshToken.CreationDate = DateTime.UtcNow; | |||
| existRefreshToken.ExpiryDate = DateTime.UtcNow.AddMinutes(_authSettings.JwtRefreshExpiredTime); | |||
| if (authenticate) | |||
| { | |||
| existRefreshToken.Used = false; | |||
| existRefreshToken.Invalidated = false; | |||
| } | |||
| //_databaseContext.RefreshTokens.Update(existRefreshToken); | |||
| await UpdateRefreshToken(existRefreshToken); | |||
| } | |||
| else | |||
| { | |||
| await _databaseContext.RefreshTokens.AddAsync(refreshToken); | |||
| } | |||
| await _databaseContext.SaveChangesAsync(); | |||
| return writedToken; | |||
| } | |||
| public async Task<RefreshTokenResultDto> RefreshTokenAsync(RefreshTokenRequestDto model) | |||
| { | |||
| var validatedToken = GetPrincipalFromToken(model.Token); | |||
| if (validatedToken == null) | |||
| { | |||
| return new RefreshTokenResultDto { Error = "Invalid token" }; | |||
| } | |||
| var expiryDateUnix = long.Parse(validatedToken.Claims.Single(x => x.Type == JwtRegisteredClaimNames.Exp).Value); | |||
| var expiryDateTimeUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc) | |||
| .AddSeconds(expiryDateUnix); | |||
| if (expiryDateTimeUtc > DateTime.UtcNow) | |||
| { | |||
| return new RefreshTokenResultDto { Error = "This token hasn't expired yet" }; | |||
| } | |||
| var jti = validatedToken.Claims.Single(x => x.Type == JwtRegisteredClaimNames.Jti).Value; | |||
| var storedRefreshToken = await _databaseContext.RefreshTokens.SingleOrDefaultAsync(x => x.Token == model.RefreshToken); | |||
| if (storedRefreshToken == null) | |||
| { | |||
| return new RefreshTokenResultDto { Error = "This refresh token does not exist" }; | |||
| } | |||
| if (DateTime.UtcNow > storedRefreshToken.ExpiryDate) | |||
| { | |||
| return new RefreshTokenResultDto { Error = "This refresh token has expired" }; | |||
| } | |||
| if (storedRefreshToken.Invalidated) | |||
| { | |||
| return new RefreshTokenResultDto { Error = "This refresh token has been invalidated" }; | |||
| } | |||
| if (storedRefreshToken.Used) | |||
| { | |||
| return new RefreshTokenResultDto { Error = "This refresh token has been used" }; | |||
| } | |||
| if (storedRefreshToken.JwtId != jti) | |||
| { | |||
| return new RefreshTokenResultDto { Error = "This refresh token does not match this JWT" }; | |||
| } | |||
| storedRefreshToken.Used = true; | |||
| _databaseContext.RefreshTokens.Update(storedRefreshToken); | |||
| await _databaseContext.SaveChangesAsync(); | |||
| var user = await _userManager.FindByIdAsync(validatedToken.Claims.Single(x => x.Type == "id").Value); | |||
| var token = await GenerateJwtToken(user); | |||
| return new RefreshTokenResultDto | |||
| { | |||
| Token = token | |||
| }; | |||
| } | |||
| private ClaimsPrincipal GetPrincipalFromToken(string token) | |||
| { | |||
| var tokenHandler = new JwtSecurityTokenHandler(); | |||
| var key = Encoding.ASCII.GetBytes(_authSettings.Secret); | |||
| var tokenValidationParameters = new TokenValidationParameters | |||
| { | |||
| ValidateIssuerSigningKey = true, | |||
| IssuerSigningKey = new SymmetricSecurityKey(key), | |||
| ValidateIssuer = false, | |||
| ValidateAudience = false, | |||
| RequireExpirationTime = false, | |||
| ValidateLifetime = true, | |||
| // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later) | |||
| //ClockSkew = TimeSpan.Zero | |||
| }; | |||
| try | |||
| { | |||
| var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out var validatedToken); | |||
| if (!IsJwtWithValidSecurityAlgorithm(validatedToken)) | |||
| { | |||
| return null; | |||
| } | |||
| return principal; | |||
| } | |||
| catch (Exception ex) | |||
| { | |||
| return null; | |||
| } | |||
| } | |||
| private bool IsJwtWithValidSecurityAlgorithm(SecurityToken validatedToken) | |||
| { | |||
| return (validatedToken is JwtSecurityToken jwtSecurityToken) && | |||
| jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, | |||
| StringComparison.InvariantCultureIgnoreCase); | |||
| } | |||
| public async Task<RefreshToken?> GetRefreshTokenByUserId(int userId) | |||
| { | |||
| return await _databaseContext.RefreshTokens.Where(x => x.UserId == userId).FirstOrDefaultAsync(); | |||
| } | |||
| public async Task UpdateRefreshToken(RefreshToken refreshToken) | |||
| { | |||
| _databaseContext.RefreshTokens.Update(refreshToken); | |||
| await _databaseContext.SaveChangesAsync(); | |||
| } | |||
| } | |||
| } | |||
| @@ -3,5 +3,9 @@ | |||
| public class AuthorizationSettings | |||
| { | |||
| public string Secret { get; set; } | |||
| public int JwtExpiredTime { get; set; } | |||
| public int JwtRefreshExpiredTime { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,13 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Diligent.WebAPI.Contracts.DTOs.Auth | |||
| { | |||
| public class AuthFailedResponse | |||
| { | |||
| public string Error { get; set; } | |||
| } | |||
| } | |||
| @@ -7,5 +7,6 @@ | |||
| public string LastName { get; set; } | |||
| public string Username { get; set; } | |||
| public string Token { get; set; } | |||
| public string RefreshToken { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,15 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Diligent.WebAPI.Contracts.DTOs.Auth | |||
| { | |||
| public class RefreshTokenRequestDto | |||
| { | |||
| public string Token { get; set; } | |||
| public string RefreshToken { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,22 @@ | |||
| | |||
| namespace Diligent.WebAPI.Contracts.DTOs.Auth | |||
| { | |||
| public class RefreshTokenResponseDto | |||
| { | |||
| public int Id { get; set; } | |||
| public string Token { get; set; } | |||
| public string JwtId { get; set; } | |||
| public DateTime CreationDate { get; set; } | |||
| public DateTime ExpiryDate { get; set; } | |||
| public bool Used { get; set; } | |||
| public bool Invalidated { get; set; } | |||
| public int UserId { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,15 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Diligent.WebAPI.Contracts.DTOs.Auth | |||
| { | |||
| public class RefreshTokenResultDto | |||
| { | |||
| public string? Token { get; set; } | |||
| public string? Error { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,10 @@ | |||
| | |||
| namespace Diligent.WebAPI.Contracts.DTOs.Auth | |||
| { | |||
| public class RefreshedTokenResponseDto | |||
| { | |||
| public string Token { get; set; } | |||
| public string RefreshToken { get; set; } | |||
| } | |||
| } | |||
| @@ -7,6 +7,7 @@ public class DatabaseContext : IdentityDbContext<User, AppRole, int> | |||
| public DbSet<InsurancePolicy> InsurancePolicies { get; set; } | |||
| public DbSet<WebhookSubscription> WebhookSubscriptions { get; set; } | |||
| public DbSet<WebhookDefinition> WebhookDefinitions { get; set; } | |||
| public DbSet<RefreshToken> RefreshTokens { get; set; } | |||
| public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options) { } | |||
| @@ -0,0 +1,32 @@ | |||
| using System; | |||
| using System.Collections.Generic; | |||
| using System.ComponentModel.DataAnnotations.Schema; | |||
| using System.Linq; | |||
| using System.Text; | |||
| using System.Threading.Tasks; | |||
| namespace Diligent.WebAPI.Data.Entities | |||
| { | |||
| public class RefreshToken | |||
| { | |||
| [Key] | |||
| public int Id { get; set; } | |||
| public string Token { get; set; } | |||
| public string JwtId { get; set; } | |||
| public DateTime CreationDate { get; set; } | |||
| public DateTime ExpiryDate { get; set; } | |||
| public bool Used { get; set; } | |||
| public bool Invalidated { get; set; } | |||
| public int UserId { get; set; } | |||
| [ForeignKey(nameof(UserId))] | |||
| public User User { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,597 @@ | |||
| // <auto-generated /> | |||
| using System; | |||
| using Diligent.WebAPI.Data; | |||
| using Microsoft.EntityFrameworkCore; | |||
| using Microsoft.EntityFrameworkCore.Infrastructure; | |||
| using Microsoft.EntityFrameworkCore.Metadata; | |||
| using Microsoft.EntityFrameworkCore.Migrations; | |||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | |||
| #nullable disable | |||
| namespace Diligent.WebAPI.Data.Migrations | |||
| { | |||
| [DbContext(typeof(DatabaseContext))] | |||
| [Migration("20221024112939_AddedRefreshToken")] | |||
| partial class AddedRefreshToken | |||
| { | |||
| protected override void BuildTargetModel(ModelBuilder modelBuilder) | |||
| { | |||
| #pragma warning disable 612, 618 | |||
| modelBuilder | |||
| .HasAnnotation("ProductVersion", "6.0.10") | |||
| .HasAnnotation("Relational:MaxIdentifierLength", 128); | |||
| SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.AppRole", b => | |||
| { | |||
| b.Property<int>("Id") | |||
| .ValueGeneratedOnAdd() | |||
| .HasColumnType("int"); | |||
| SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1); | |||
| b.Property<string>("ConcurrencyStamp") | |||
| .IsConcurrencyToken() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("Name") | |||
| .HasMaxLength(256) | |||
| .HasColumnType("nvarchar(256)"); | |||
| b.Property<string>("NormalizedName") | |||
| .HasMaxLength(256) | |||
| .HasColumnType("nvarchar(256)"); | |||
| b.HasKey("Id"); | |||
| b.HasIndex("NormalizedName") | |||
| .IsUnique() | |||
| .HasDatabaseName("RoleNameIndex") | |||
| .HasFilter("[NormalizedName] IS NOT NULL"); | |||
| b.ToTable("AspNetRoles", (string)null); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.InsuranceCompany", b => | |||
| { | |||
| b.Property<long>("Id") | |||
| .ValueGeneratedOnAdd() | |||
| .HasColumnType("bigint"); | |||
| SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"), 1L, 1); | |||
| b.Property<string>("City") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("Country") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<DateTime>("CreatedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<DateTime?>("DeletedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<string>("Fax") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("LegalAddress") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("LegalEmail") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("Name") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("PhoneNumber") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("PostalCode") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<DateTime?>("UpdatedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.HasKey("Id"); | |||
| b.ToTable("InsuranceCompanies"); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.InsurancePolicy", b => | |||
| { | |||
| b.Property<long>("Id") | |||
| .ValueGeneratedOnAdd() | |||
| .HasColumnType("bigint"); | |||
| SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"), 1L, 1); | |||
| b.Property<DateTime>("CreatedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<DateTime?>("DeletedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<DateTime>("EndDate") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<long>("InsurerId") | |||
| .HasColumnType("bigint"); | |||
| b.Property<decimal>("Premium") | |||
| .HasColumnType("decimal(18,2)"); | |||
| b.Property<DateTime>("StartDate") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<string>("Type") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<DateTime?>("UpdatedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.HasKey("Id"); | |||
| b.HasIndex("InsurerId"); | |||
| b.ToTable("InsurancePolicies"); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.Insurer", b => | |||
| { | |||
| b.Property<long>("Id") | |||
| .ValueGeneratedOnAdd() | |||
| .HasColumnType("bigint"); | |||
| SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"), 1L, 1); | |||
| b.Property<string>("Address") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("City") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("Country") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<DateTime>("CreatedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<DateTime>("DateOfBirth") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<DateTime?>("DeletedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<string>("Email") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("FirstName") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<long>("InsuranceCompanyId") | |||
| .HasColumnType("bigint"); | |||
| b.Property<string>("LastName") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("PhoneNumber") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("PostalCode") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<DateTime?>("UpdatedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.HasKey("Id"); | |||
| b.HasIndex("InsuranceCompanyId"); | |||
| b.ToTable("Insurers"); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.RefreshToken", b => | |||
| { | |||
| b.Property<int>("Id") | |||
| .ValueGeneratedOnAdd() | |||
| .HasColumnType("int"); | |||
| SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1); | |||
| b.Property<DateTime>("CreationDate") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<DateTime>("ExpiryDate") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<bool>("Invalidated") | |||
| .HasColumnType("bit"); | |||
| b.Property<string>("JwtId") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("Token") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<bool>("Used") | |||
| .HasColumnType("bit"); | |||
| b.Property<int>("UserId") | |||
| .HasColumnType("int"); | |||
| b.HasKey("Id"); | |||
| b.HasIndex("UserId"); | |||
| b.ToTable("RefreshTokens"); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.User", b => | |||
| { | |||
| b.Property<int>("Id") | |||
| .ValueGeneratedOnAdd() | |||
| .HasColumnType("int"); | |||
| SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1); | |||
| b.Property<int>("AccessFailedCount") | |||
| .HasColumnType("int"); | |||
| b.Property<string>("ConcurrencyStamp") | |||
| .IsConcurrencyToken() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("Email") | |||
| .HasMaxLength(256) | |||
| .HasColumnType("nvarchar(256)"); | |||
| b.Property<bool>("EmailConfirmed") | |||
| .HasColumnType("bit"); | |||
| b.Property<string>("FirstName") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("LastName") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<bool>("LockoutEnabled") | |||
| .HasColumnType("bit"); | |||
| b.Property<DateTimeOffset?>("LockoutEnd") | |||
| .HasColumnType("datetimeoffset"); | |||
| b.Property<string>("NormalizedEmail") | |||
| .HasMaxLength(256) | |||
| .HasColumnType("nvarchar(256)"); | |||
| b.Property<string>("NormalizedUserName") | |||
| .HasMaxLength(256) | |||
| .HasColumnType("nvarchar(256)"); | |||
| b.Property<string>("PasswordHash") | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("PhoneNumber") | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<bool>("PhoneNumberConfirmed") | |||
| .HasColumnType("bit"); | |||
| b.Property<string>("SecurityStamp") | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<bool>("TwoFactorEnabled") | |||
| .HasColumnType("bit"); | |||
| b.Property<string>("UserName") | |||
| .HasMaxLength(256) | |||
| .HasColumnType("nvarchar(256)"); | |||
| b.HasKey("Id"); | |||
| b.HasIndex("NormalizedEmail") | |||
| .HasDatabaseName("EmailIndex"); | |||
| b.HasIndex("NormalizedUserName") | |||
| .IsUnique() | |||
| .HasDatabaseName("UserNameIndex") | |||
| .HasFilter("[NormalizedUserName] IS NOT NULL"); | |||
| b.ToTable("AspNetUsers", (string)null); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.WebhookDefinition", b => | |||
| { | |||
| b.Property<long>("Id") | |||
| .ValueGeneratedOnAdd() | |||
| .HasColumnType("bigint"); | |||
| SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"), 1L, 1); | |||
| b.Property<DateTime>("CreatedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<DateTime?>("DeletedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<string>("Description") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("DisplayName") | |||
| .IsRequired() | |||
| .HasMaxLength(100) | |||
| .HasColumnType("nvarchar(100)"); | |||
| b.Property<string>("Name") | |||
| .IsRequired() | |||
| .HasMaxLength(100) | |||
| .HasColumnType("nvarchar(100)"); | |||
| b.Property<DateTime?>("UpdatedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.HasKey("Id"); | |||
| b.ToTable("WebhookDefinitions"); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.WebhookSubscription", b => | |||
| { | |||
| b.Property<long>("Id") | |||
| .ValueGeneratedOnAdd() | |||
| .HasColumnType("bigint"); | |||
| SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"), 1L, 1); | |||
| b.Property<DateTime>("CreatedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<DateTime?>("DeletedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<bool>("IsActive") | |||
| .HasColumnType("bit"); | |||
| b.Property<DateTime?>("UpdatedAtUtc") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<long>("WebhookDefinitionId") | |||
| .HasColumnType("bigint"); | |||
| b.Property<string>("WebhookURL") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.HasKey("Id"); | |||
| b.HasIndex("WebhookDefinitionId"); | |||
| b.ToTable("WebhookSubscriptions"); | |||
| }); | |||
| modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b => | |||
| { | |||
| b.Property<int>("Id") | |||
| .ValueGeneratedOnAdd() | |||
| .HasColumnType("int"); | |||
| SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1); | |||
| b.Property<string>("ClaimType") | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("ClaimValue") | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<int>("RoleId") | |||
| .HasColumnType("int"); | |||
| b.HasKey("Id"); | |||
| b.HasIndex("RoleId"); | |||
| b.ToTable("AspNetRoleClaims", (string)null); | |||
| }); | |||
| modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b => | |||
| { | |||
| b.Property<int>("Id") | |||
| .ValueGeneratedOnAdd() | |||
| .HasColumnType("int"); | |||
| SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1); | |||
| b.Property<string>("ClaimType") | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("ClaimValue") | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<int>("UserId") | |||
| .HasColumnType("int"); | |||
| b.HasKey("Id"); | |||
| b.HasIndex("UserId"); | |||
| b.ToTable("AspNetUserClaims", (string)null); | |||
| }); | |||
| modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b => | |||
| { | |||
| b.Property<string>("LoginProvider") | |||
| .HasColumnType("nvarchar(450)"); | |||
| b.Property<string>("ProviderKey") | |||
| .HasColumnType("nvarchar(450)"); | |||
| b.Property<string>("ProviderDisplayName") | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<int>("UserId") | |||
| .HasColumnType("int"); | |||
| b.HasKey("LoginProvider", "ProviderKey"); | |||
| b.HasIndex("UserId"); | |||
| b.ToTable("AspNetUserLogins", (string)null); | |||
| }); | |||
| modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<int>", b => | |||
| { | |||
| b.Property<int>("UserId") | |||
| .HasColumnType("int"); | |||
| b.Property<int>("RoleId") | |||
| .HasColumnType("int"); | |||
| b.HasKey("UserId", "RoleId"); | |||
| b.HasIndex("RoleId"); | |||
| b.ToTable("AspNetUserRoles", (string)null); | |||
| }); | |||
| modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b => | |||
| { | |||
| b.Property<int>("UserId") | |||
| .HasColumnType("int"); | |||
| b.Property<string>("LoginProvider") | |||
| .HasColumnType("nvarchar(450)"); | |||
| b.Property<string>("Name") | |||
| .HasColumnType("nvarchar(450)"); | |||
| b.Property<string>("Value") | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.HasKey("UserId", "LoginProvider", "Name"); | |||
| b.ToTable("AspNetUserTokens", (string)null); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.InsurancePolicy", b => | |||
| { | |||
| b.HasOne("Diligent.WebAPI.Data.Entities.Insurer", "Insurer") | |||
| .WithMany() | |||
| .HasForeignKey("InsurerId") | |||
| .OnDelete(DeleteBehavior.Cascade) | |||
| .IsRequired(); | |||
| b.Navigation("Insurer"); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.Insurer", b => | |||
| { | |||
| b.HasOne("Diligent.WebAPI.Data.Entities.InsuranceCompany", "InsuranceCompany") | |||
| .WithMany() | |||
| .HasForeignKey("InsuranceCompanyId") | |||
| .OnDelete(DeleteBehavior.Cascade) | |||
| .IsRequired(); | |||
| b.Navigation("InsuranceCompany"); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.RefreshToken", b => | |||
| { | |||
| b.HasOne("Diligent.WebAPI.Data.Entities.User", "User") | |||
| .WithMany() | |||
| .HasForeignKey("UserId") | |||
| .OnDelete(DeleteBehavior.Cascade) | |||
| .IsRequired(); | |||
| b.Navigation("User"); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.WebhookSubscription", b => | |||
| { | |||
| b.HasOne("Diligent.WebAPI.Data.Entities.WebhookDefinition", "WebhookDefinition") | |||
| .WithMany() | |||
| .HasForeignKey("WebhookDefinitionId") | |||
| .OnDelete(DeleteBehavior.Cascade) | |||
| .IsRequired(); | |||
| b.Navigation("WebhookDefinition"); | |||
| }); | |||
| modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b => | |||
| { | |||
| b.HasOne("Diligent.WebAPI.Data.Entities.AppRole", null) | |||
| .WithMany() | |||
| .HasForeignKey("RoleId") | |||
| .OnDelete(DeleteBehavior.Cascade) | |||
| .IsRequired(); | |||
| }); | |||
| modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b => | |||
| { | |||
| b.HasOne("Diligent.WebAPI.Data.Entities.User", null) | |||
| .WithMany() | |||
| .HasForeignKey("UserId") | |||
| .OnDelete(DeleteBehavior.Cascade) | |||
| .IsRequired(); | |||
| }); | |||
| modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b => | |||
| { | |||
| b.HasOne("Diligent.WebAPI.Data.Entities.User", null) | |||
| .WithMany() | |||
| .HasForeignKey("UserId") | |||
| .OnDelete(DeleteBehavior.Cascade) | |||
| .IsRequired(); | |||
| }); | |||
| modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<int>", b => | |||
| { | |||
| b.HasOne("Diligent.WebAPI.Data.Entities.AppRole", null) | |||
| .WithMany() | |||
| .HasForeignKey("RoleId") | |||
| .OnDelete(DeleteBehavior.Cascade) | |||
| .IsRequired(); | |||
| b.HasOne("Diligent.WebAPI.Data.Entities.User", null) | |||
| .WithMany() | |||
| .HasForeignKey("UserId") | |||
| .OnDelete(DeleteBehavior.Cascade) | |||
| .IsRequired(); | |||
| }); | |||
| modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b => | |||
| { | |||
| b.HasOne("Diligent.WebAPI.Data.Entities.User", null) | |||
| .WithMany() | |||
| .HasForeignKey("UserId") | |||
| .OnDelete(DeleteBehavior.Cascade) | |||
| .IsRequired(); | |||
| }); | |||
| #pragma warning restore 612, 618 | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,49 @@ | |||
| using System; | |||
| using Microsoft.EntityFrameworkCore.Migrations; | |||
| #nullable disable | |||
| namespace Diligent.WebAPI.Data.Migrations | |||
| { | |||
| public partial class AddedRefreshToken : Migration | |||
| { | |||
| protected override void Up(MigrationBuilder migrationBuilder) | |||
| { | |||
| migrationBuilder.CreateTable( | |||
| name: "RefreshTokens", | |||
| columns: table => new | |||
| { | |||
| Id = table.Column<int>(type: "int", nullable: false) | |||
| .Annotation("SqlServer:Identity", "1, 1"), | |||
| Token = table.Column<string>(type: "nvarchar(max)", nullable: false), | |||
| JwtId = table.Column<string>(type: "nvarchar(max)", nullable: false), | |||
| CreationDate = table.Column<DateTime>(type: "datetime2", nullable: false), | |||
| ExpiryDate = table.Column<DateTime>(type: "datetime2", nullable: false), | |||
| Used = table.Column<bool>(type: "bit", nullable: false), | |||
| Invalidated = table.Column<bool>(type: "bit", nullable: false), | |||
| UserId = table.Column<int>(type: "int", nullable: false) | |||
| }, | |||
| constraints: table => | |||
| { | |||
| table.PrimaryKey("PK_RefreshTokens", x => x.Id); | |||
| table.ForeignKey( | |||
| name: "FK_RefreshTokens_AspNetUsers_UserId", | |||
| column: x => x.UserId, | |||
| principalTable: "AspNetUsers", | |||
| principalColumn: "Id", | |||
| onDelete: ReferentialAction.Cascade); | |||
| }); | |||
| migrationBuilder.CreateIndex( | |||
| name: "IX_RefreshTokens_UserId", | |||
| table: "RefreshTokens", | |||
| column: "UserId"); | |||
| } | |||
| protected override void Down(MigrationBuilder migrationBuilder) | |||
| { | |||
| migrationBuilder.DropTable( | |||
| name: "RefreshTokens"); | |||
| } | |||
| } | |||
| } | |||
| @@ -208,6 +208,44 @@ namespace Diligent.WebAPI.Data.Migrations | |||
| b.ToTable("Insurers"); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.RefreshToken", b => | |||
| { | |||
| b.Property<int>("Id") | |||
| .ValueGeneratedOnAdd() | |||
| .HasColumnType("int"); | |||
| SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1); | |||
| b.Property<DateTime>("CreationDate") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<DateTime>("ExpiryDate") | |||
| .HasColumnType("datetime2"); | |||
| b.Property<bool>("Invalidated") | |||
| .HasColumnType("bit"); | |||
| b.Property<string>("JwtId") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<string>("Token") | |||
| .IsRequired() | |||
| .HasColumnType("nvarchar(max)"); | |||
| b.Property<bool>("Used") | |||
| .HasColumnType("bit"); | |||
| b.Property<int>("UserId") | |||
| .HasColumnType("int"); | |||
| b.HasKey("Id"); | |||
| b.HasIndex("UserId"); | |||
| b.ToTable("RefreshTokens"); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.User", b => | |||
| { | |||
| b.Property<int>("Id") | |||
| @@ -479,6 +517,17 @@ namespace Diligent.WebAPI.Data.Migrations | |||
| b.Navigation("InsuranceCompany"); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.RefreshToken", b => | |||
| { | |||
| b.HasOne("Diligent.WebAPI.Data.Entities.User", "User") | |||
| .WithMany() | |||
| .HasForeignKey("UserId") | |||
| .OnDelete(DeleteBehavior.Cascade) | |||
| .IsRequired(); | |||
| b.Navigation("User"); | |||
| }); | |||
| modelBuilder.Entity("Diligent.WebAPI.Data.Entities.WebhookSubscription", b => | |||
| { | |||
| b.HasOne("Diligent.WebAPI.Data.Entities.WebhookDefinition", "WebhookDefinition") | |||
| @@ -37,5 +37,18 @@ | |||
| return Ok(response.Data); | |||
| } | |||
| [HttpPost("refresh")] | |||
| public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenRequestDto model) | |||
| { | |||
| var response = await _userService.RefreshTokenAsync(model); | |||
| if (response.Error != null) | |||
| { | |||
| return BadRequest(new AuthFailedResponse { Error = response.Error }); | |||
| } | |||
| return Ok(new RefreshedTokenResponseDto { Token = response.Token, RefreshToken = response.Token }); | |||
| } | |||
| } | |||
| } | |||
| @@ -16,12 +16,12 @@ | |||
| var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last(); | |||
| if (token != null) | |||
| AttachUserToContext(context, userService, token); | |||
| await AttachUserToContext(context, userService, token); | |||
| await _next(context); | |||
| } | |||
| private void AttachUserToContext(HttpContext context, IUserService userService, string token) | |||
| private async Task AttachUserToContext(HttpContext context, IUserService userService, string token) | |||
| { | |||
| try | |||
| { | |||
| @@ -33,15 +33,19 @@ | |||
| IssuerSigningKey = new SymmetricSecurityKey(key), | |||
| ValidateIssuer = false, | |||
| ValidateAudience = false, | |||
| RequireExpirationTime = false, | |||
| ValidateLifetime = true, | |||
| // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later) | |||
| ClockSkew = TimeSpan.Zero | |||
| //ClockSkew = TimeSpan.Zero | |||
| }, out SecurityToken validatedToken); | |||
| var jwtToken = (JwtSecurityToken)validatedToken; | |||
| var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value); | |||
| // attach user to context on successful jwt validation | |||
| context.Items["User"] = userService.GetById(userId); | |||
| context.Items["User"] = await userService.GetById(userId); | |||
| await UpdateRefreshToken(context, userService, userId); | |||
| } | |||
| catch | |||
| { | |||
| @@ -49,5 +53,18 @@ | |||
| // user is not attached to context so request won't have access to secure routes | |||
| } | |||
| } | |||
| private async Task UpdateRefreshToken(HttpContext context, IUserService userService, int userId) | |||
| { | |||
| var refreshToken = await userService.GetRefreshTokenByUserId(userId); | |||
| if (refreshToken == null) | |||
| return; | |||
| refreshToken.ExpiryDate = DateTime.UtcNow.AddMinutes(30); | |||
| await userService.UpdateRefreshToken(refreshToken); | |||
| } | |||
| } | |||
| } | |||
| @@ -3,6 +3,8 @@ | |||
| "WebApi": "Server=.;Database=HRCenter;Trusted_Connection=True;MultipleActiveResultSets=true" | |||
| }, | |||
| "Authorization": { | |||
| "JwtExpiredTime": "5", | |||
| "JwtRefreshExpiredTime": "30", | |||
| "Secret": "SECRET_ASKGFH#$_#((Y)#I%EWJGDSJTGKEOS@$SAF" | |||
| } | |||
| } | |||