diff --git a/IdentityServer.Api/Startup.cs b/IdentityServer.Api/Startup.cs index 3063214..92737ab 100644 --- a/IdentityServer.Api/Startup.cs +++ b/IdentityServer.Api/Startup.cs @@ -1,5 +1,6 @@ using AutoMapper; using IdentityServer.Application; +using IdentityServer.Application.Services.Abstractions; using IdentityServer.Domain.Data; using MediatR; using MediatR.Pipeline; @@ -76,6 +77,9 @@ namespace IdentityServer.Api endpoints.MapControllers(); }); app.ConfigureSwagger("IdentityServer API"); + + var behaviorService = app.ApplicationServices.GetService(); + behaviorService.FillTokenStore(); } } } diff --git a/IdentityServer.Api/appsettings.json b/IdentityServer.Api/appsettings.json index 88d9220..116b2e5 100644 --- a/IdentityServer.Api/appsettings.json +++ b/IdentityServer.Api/appsettings.json @@ -10,5 +10,11 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "Restrictions": { + "MaxFailedLoginAttempts": 5 + }, + "Token": { + "ValidityInMinutes": 43800 + } } diff --git a/IdentityServer.Application/DependencyInjectionExtensions.cs b/IdentityServer.Application/DependencyInjectionExtensions.cs index 3fd4797..a987a02 100644 --- a/IdentityServer.Application/DependencyInjectionExtensions.cs +++ b/IdentityServer.Application/DependencyInjectionExtensions.cs @@ -1,5 +1,7 @@ using IdentityServer.Application.Services; +using IdentityServer.Application.Services.Abstractions; using IdentityServer.Application.Stores; +using IdentityServer.Domain.Abstractions; using Microsoft.Extensions.DependencyInjection; namespace IdentityServer.Application @@ -9,13 +11,15 @@ namespace IdentityServer.Application public static void AddApplicationServices(this IServiceCollection services) { services.AddStores(); + services.AddSingleton(); services.AddSingleton(); services.AddScoped(); + services.AddScoped(); } private static void AddStores(this IServiceCollection services) { - services.AddSingleton(); + services.AddSingleton(); } } } diff --git a/IdentityServer.Application/Services/Abstractions/IBehaviorService.cs b/IdentityServer.Application/Services/Abstractions/IBehaviorService.cs new file mode 100644 index 0000000..7a7c7b9 --- /dev/null +++ b/IdentityServer.Application/Services/Abstractions/IBehaviorService.cs @@ -0,0 +1,7 @@ +namespace IdentityServer.Application.Services.Abstractions +{ + public interface IBehaviorService + { + void FillTokenStore(); + } +} diff --git a/IdentityServer.Application/Services/BehaviorService.cs b/IdentityServer.Application/Services/BehaviorService.cs new file mode 100644 index 0000000..48257a6 --- /dev/null +++ b/IdentityServer.Application/Services/BehaviorService.cs @@ -0,0 +1,36 @@ +using IdentityServer.Application.Services.Abstractions; +using IdentityServer.Application.Stores; +using IdentityServer.Domain.Models; +using IdentityServer.Domain.Repositories; +using System.Threading.Tasks; + +namespace IdentityServer.Application.Services +{ + internal class BehaviorService : IBehaviorService + { + private readonly ITokenStore _securityStore; + private readonly IIdentityRepository _identityRepository; + + public BehaviorService(ITokenStore securityStore, IIdentityRepository identityRepository) + { + _securityStore = securityStore; + _identityRepository = identityRepository; + } + + public void FillTokenStore() + => FillTokenStoreAsync().GetAwaiter().GetResult(); + + public async Task FillTokenStoreAsync() + { + var activeTokens = await _identityRepository.GetActiveTokens(); + if (activeTokens.Length <= 0) + return; + + foreach (var token in activeTokens) + { + var storeToken = new Token() { Raw = token.Token, ValidFrom = token.ValidFrom, ValidUntil = token.ValidUntil }; + _securityStore.SetToken(storeToken, token.UserId); + } + } + } +} diff --git a/IdentityServer.Application/Services/ConfigProvider.cs b/IdentityServer.Application/Services/ConfigProvider.cs new file mode 100644 index 0000000..755a02b --- /dev/null +++ b/IdentityServer.Application/Services/ConfigProvider.cs @@ -0,0 +1,19 @@ +using IdentityServer.Domain.Abstractions; +using IdentityServer.Domain.Models.Config; +using Microsoft.Extensions.Configuration; + +namespace IdentityServer.Application.Services +{ + internal class ConfigProvider : IConfigProvider + { + private readonly IConfiguration _configuration; + + public ConfigProvider(IConfiguration configuration) + { + _configuration=configuration; + } + + public RestrictionsConfig Restrictions => _configuration.GetSection("Restrictions").Get(); + public TokenConfig Token => _configuration.GetSection("Token").Get(); + } +} diff --git a/IdentityServer.Application/Services/ITokenService.cs b/IdentityServer.Application/Services/ITokenService.cs index e4e5361..7ec21e9 100644 --- a/IdentityServer.Application/Services/ITokenService.cs +++ b/IdentityServer.Application/Services/ITokenService.cs @@ -5,7 +5,7 @@ namespace IdentityServer.Application.Services { internal interface ITokenService { - string GenerateTokenRaw(AppUser user); + Token GenerateToken(AppUser user); TokenCore ExtractTokenCore(string tokenRaw); } } \ No newline at end of file diff --git a/IdentityServer.Application/Services/TokenService.cs b/IdentityServer.Application/Services/TokenService.cs index 37a73ed..f3a32b0 100644 --- a/IdentityServer.Application/Services/TokenService.cs +++ b/IdentityServer.Application/Services/TokenService.cs @@ -1,4 +1,5 @@ using AutoMapper; +using IdentityServer.Domain.Abstractions; using IdentityServer.Domain.Entities; using IdentityServer.Domain.Models; using Newtonsoft.Json; @@ -11,22 +12,32 @@ namespace IdentityServer.Application.Services internal class TokenService : ITokenService { private readonly IMapper _mapper; + private readonly IConfigProvider _configProvider; - public TokenService(IMapper mapper) + public TokenService(IMapper mapper, IConfigProvider configProvider) { _mapper = mapper; + _configProvider = configProvider; } - public string GenerateTokenRaw(AppUser user) + public Token GenerateToken(AppUser user) { - var tokenCore = GenerateToken(user); + var tokenRaw = GenerateTokenRaw(user); + var currentDate = DateTime.Now; + var token = new Token() { Raw = tokenRaw, ValidFrom = currentDate, ValidUntil = currentDate.AddMinutes(_configProvider.Token.ValidityInMinutes) }; + return token; + } + + private string GenerateTokenRaw(AppUser user) + { + var tokenCore = GenerateTokenCore(user); var tokenCoreString = JsonConvert.SerializeObject(tokenCore); var tokenCoreBytes = Encoding.UTF8.GetBytes(tokenCoreString); var tokenRaw = Convert.ToBase64String(tokenCoreBytes); return tokenRaw; } - private TokenCore GenerateToken(AppUser user) + private TokenCore GenerateTokenCore(AppUser user) { var tokenCore = _mapper.Map(user); tokenCore.LockStamp = Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", ""); diff --git a/IdentityServer.Application/Services/UserService.cs b/IdentityServer.Application/Services/UserService.cs index d96e064..49f9a40 100644 --- a/IdentityServer.Application/Services/UserService.cs +++ b/IdentityServer.Application/Services/UserService.cs @@ -1,4 +1,6 @@ using IdentityServer.Application.Stores; +using IdentityServer.Domain.Abstractions; +using IdentityServer.Domain.Entities; using IdentityServer.Domain.Models; using IdentityServer.Domain.Repositories; using System; @@ -8,26 +10,27 @@ namespace IdentityServer.Application.Services { internal class UserService : IUserService { - private readonly ISecurityStore _securityStore; + private readonly ITokenStore _securityStore; private readonly IIdentityRepository _identityRepository; private readonly ITokenService _tokenService; + private readonly IConfigProvider _configProvider; - public UserService(ISecurityStore securityStore, IIdentityRepository identityRepository, ITokenService tokenService) + public UserService(ITokenStore securityStore, IIdentityRepository identityRepository, ITokenService tokenService, IConfigProvider configProvider) { _securityStore = securityStore; _identityRepository = identityRepository; _tokenService = tokenService; + _configProvider = configProvider; } public async Task Authenticate(string userName, string password) { var user = await _identityRepository.GetUser(userName, password); - if (user == null) + var valid = ValidateUser(user); + if (!valid) return null; - var tokenRaw = _tokenService.GenerateTokenRaw(user); - var currentDate = DateTime.Now; - var token = new Token() { Raw = tokenRaw, ValidFrom = currentDate, ValidUntil = currentDate.AddMonths(12) }; + var token = _tokenService.GenerateToken(user); _securityStore.SetToken(token, user.UserId); await _identityRepository.UpdateUserAfterAuthentication(user, token); @@ -42,5 +45,16 @@ namespace IdentityServer.Application.Services return tokenCore; } + + private bool ValidateUser(AppUser user) + { + if (user == null) + return false; + + if (user.FailedLoginAttempts.HasValue && user.FailedLoginAttempts.Value > _configProvider.Restrictions.MaxFailedLoginAttempts) + return false; + + return true; + } } } diff --git a/IdentityServer.Application/Stores/ISecurityStore.cs b/IdentityServer.Application/Stores/ITokenStore.cs similarity index 84% rename from IdentityServer.Application/Stores/ISecurityStore.cs rename to IdentityServer.Application/Stores/ITokenStore.cs index a67f668..c61ef10 100644 --- a/IdentityServer.Application/Stores/ISecurityStore.cs +++ b/IdentityServer.Application/Stores/ITokenStore.cs @@ -2,7 +2,7 @@ namespace IdentityServer.Application.Stores { - internal interface ISecurityStore + internal interface ITokenStore { void SetToken(Token token, int userId); TokenCore ValidateAndGetTokenCore(string token); diff --git a/IdentityServer.Application/Stores/SecurityStore.cs b/IdentityServer.Application/Stores/TokenStore.cs similarity index 92% rename from IdentityServer.Application/Stores/SecurityStore.cs rename to IdentityServer.Application/Stores/TokenStore.cs index 5db8457..f7bc716 100644 --- a/IdentityServer.Application/Stores/SecurityStore.cs +++ b/IdentityServer.Application/Stores/TokenStore.cs @@ -6,12 +6,12 @@ using System.Linq; namespace IdentityServer.Application.Stores { - internal class SecurityStore : ISecurityStore + internal class TokenStore : ITokenStore { private readonly ITokenService _tokenService; private ConcurrentDictionary> Tokens { get; } - public SecurityStore(ITokenService tokenService) + public TokenStore(ITokenService tokenService) { _tokenService = tokenService; Tokens = new ConcurrentDictionary>(); diff --git a/IdentityServer.Domain.Data/EntityTypeConfiguration/UserTokenConfiguration.cs b/IdentityServer.Domain.Data/EntityTypeConfiguration/UserTokenConfiguration.cs index 9343328..38a5d52 100644 --- a/IdentityServer.Domain.Data/EntityTypeConfiguration/UserTokenConfiguration.cs +++ b/IdentityServer.Domain.Data/EntityTypeConfiguration/UserTokenConfiguration.cs @@ -8,8 +8,8 @@ namespace IdentityServer.Domain.Data.EntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ToTable("UserToken").HasKey(z => z.Id); - builder.Property(z => z.Id).ValueGeneratedOnAdd(); + builder.ToTable("UserToken").HasKey(z => z.TokenId); + builder.Property(z => z.TokenId).ValueGeneratedOnAdd(); } } } diff --git a/IdentityServer.Domain.Data/Repositories/IdentityRepository.cs b/IdentityServer.Domain.Data/Repositories/IdentityRepository.cs index 0ce543d..2883380 100644 --- a/IdentityServer.Domain.Data/Repositories/IdentityRepository.cs +++ b/IdentityServer.Domain.Data/Repositories/IdentityRepository.cs @@ -4,6 +4,7 @@ using IdentityServer.Domain.Models; using IdentityServer.Domain.Repositories; using Microsoft.EntityFrameworkCore; using System; +using System.Linq; using System.Threading.Tasks; namespace IdentityServer.Domain.Data.Repositories @@ -41,5 +42,13 @@ namespace IdentityServer.Domain.Data.Repositories await _dbContext.SaveChangesAsync(); } + + public Task GetActiveTokens() + { + var currentDate = DateTime.Now; + var query = _dbContext.UserTokens + .Where(z => z.ValidFrom <= currentDate && z.ValidUntil >= currentDate && (!z.Burnt.HasValue || z.Burnt.Value == false)); + return query.ToArrayAsync(); + } } } diff --git a/IdentityServer.Domain.Data/Scripts/1.0.1/04.UserToken table.sql b/IdentityServer.Domain.Data/Scripts/1.0.1/04.UserToken table.sql index baa31d7..b417c40 100644 --- a/IdentityServer.Domain.Data/Scripts/1.0.1/04.UserToken table.sql +++ b/IdentityServer.Domain.Data/Scripts/1.0.1/04.UserToken table.sql @@ -2,11 +2,12 @@ if not exists (select top 1 1 from sys.objects where name = 'UserToken' and type begin create table UserToken ( - Id int identity(1, 1) constraint PK_Token primary key, + TokenId int identity(1, 1) constraint PK_Token primary key, UserId int not null constraint FK_Token_AppUser foreign key references AppUser(UserId), Token varchar(1000) not null, ValidFrom datetime not null, - ValidUntil datetime not null + ValidUntil datetime not null, + Burnt bit ) end go \ No newline at end of file diff --git a/IdentityServer.Domain/Abstractions/IConfigProvider.cs b/IdentityServer.Domain/Abstractions/IConfigProvider.cs new file mode 100644 index 0000000..c2ec84a --- /dev/null +++ b/IdentityServer.Domain/Abstractions/IConfigProvider.cs @@ -0,0 +1,10 @@ +using IdentityServer.Domain.Models.Config; + +namespace IdentityServer.Domain.Abstractions +{ + public interface IConfigProvider + { + RestrictionsConfig Restrictions { get; } + TokenConfig Token { get; } + } +} diff --git a/IdentityServer.Domain/Entities/UserToken.cs b/IdentityServer.Domain/Entities/UserToken.cs index 5576ca6..bde0b50 100644 --- a/IdentityServer.Domain/Entities/UserToken.cs +++ b/IdentityServer.Domain/Entities/UserToken.cs @@ -4,10 +4,11 @@ namespace IdentityServer.Domain.Entities { public class UserToken { - public int Id { get; set; } + public int TokenId { get; set; } public int UserId { get; set; } public string Token { get; set; } public DateTime ValidFrom { get; set; } public DateTime ValidUntil { get; set; } + public bool? Burnt { get; set; } } } diff --git a/IdentityServer.Domain/Models/Config/RestrictionsConfig.cs b/IdentityServer.Domain/Models/Config/RestrictionsConfig.cs new file mode 100644 index 0000000..c51cbe4 --- /dev/null +++ b/IdentityServer.Domain/Models/Config/RestrictionsConfig.cs @@ -0,0 +1,7 @@ +namespace IdentityServer.Domain.Models.Config +{ + public class RestrictionsConfig + { + public int MaxFailedLoginAttempts { get; set; } + } +} diff --git a/IdentityServer.Domain/Models/Config/TokenConfig.cs b/IdentityServer.Domain/Models/Config/TokenConfig.cs new file mode 100644 index 0000000..e6e65d7 --- /dev/null +++ b/IdentityServer.Domain/Models/Config/TokenConfig.cs @@ -0,0 +1,7 @@ +namespace IdentityServer.Domain.Models.Config +{ + public class TokenConfig + { + public int ValidityInMinutes { get; set; } + } +} diff --git a/IdentityServer.Domain/Repositories/IIdentityRepository.cs b/IdentityServer.Domain/Repositories/IIdentityRepository.cs index 3278867..a605378 100644 --- a/IdentityServer.Domain/Repositories/IIdentityRepository.cs +++ b/IdentityServer.Domain/Repositories/IIdentityRepository.cs @@ -8,5 +8,6 @@ namespace IdentityServer.Domain.Repositories { Task GetUser(string userName, string password); Task UpdateUserAfterAuthentication(AppUser user, Token token); + Task GetActiveTokens(); } } diff --git a/ReleaseNotes.xml b/ReleaseNotes.xml index fb46419..c5fcd50 100644 --- a/ReleaseNotes.xml +++ b/ReleaseNotes.xml @@ -20,6 +20,7 @@ ◾ Token improvements and hard changes ◾ Increase user information complexity ◾ New token structure and generation mechanism + ◾ Persist tokens and reload active ones at server restart \ No newline at end of file