Persist user token

master
Tudor Stanciu 2021-11-13 17:17:13 +02:00
parent 36d23aa924
commit 19915f05d8
20 changed files with 159 additions and 21 deletions

View File

@ -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<IBehaviorService>();
behaviorService.FillTokenStore();
}
}
}

View File

@ -10,5 +10,11 @@
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"Restrictions": {
"MaxFailedLoginAttempts": 5
},
"Token": {
"ValidityInMinutes": 43800
}
}

View File

@ -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<IConfigProvider, ConfigProvider>();
services.AddSingleton<ITokenService, TokenService>();
services.AddScoped<IUserService, UserService>();
services.AddScoped<IBehaviorService, BehaviorService>();
}
private static void AddStores(this IServiceCollection services)
{
services.AddSingleton<ISecurityStore, SecurityStore>();
services.AddSingleton<ITokenStore, TokenStore>();
}
}
}

View File

@ -0,0 +1,7 @@
namespace IdentityServer.Application.Services.Abstractions
{
public interface IBehaviorService
{
void FillTokenStore();
}
}

View File

@ -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);
}
}
}
}

View File

@ -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<RestrictionsConfig>();
public TokenConfig Token => _configuration.GetSection("Token").Get<TokenConfig>();
}
}

View File

@ -5,7 +5,7 @@ namespace IdentityServer.Application.Services
{
internal interface ITokenService
{
string GenerateTokenRaw(AppUser user);
Token GenerateToken(AppUser user);
TokenCore ExtractTokenCore(string tokenRaw);
}
}

View File

@ -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<TokenCore>(user);
tokenCore.LockStamp = Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", "");

View File

@ -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<Token> 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;
}
}
}

View File

@ -2,7 +2,7 @@
namespace IdentityServer.Application.Stores
{
internal interface ISecurityStore
internal interface ITokenStore
{
void SetToken(Token token, int userId);
TokenCore ValidateAndGetTokenCore(string token);

View File

@ -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<int, List<Token>> Tokens { get; }
public SecurityStore(ITokenService tokenService)
public TokenStore(ITokenService tokenService)
{
_tokenService = tokenService;
Tokens = new ConcurrentDictionary<int, List<Token>>();

View File

@ -8,8 +8,8 @@ namespace IdentityServer.Domain.Data.EntityTypeConfiguration
{
public void Configure(EntityTypeBuilder<UserToken> 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();
}
}
}

View File

@ -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<UserToken[]> 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();
}
}
}

View File

@ -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

View File

@ -0,0 +1,10 @@
using IdentityServer.Domain.Models.Config;
namespace IdentityServer.Domain.Abstractions
{
public interface IConfigProvider
{
RestrictionsConfig Restrictions { get; }
TokenConfig Token { get; }
}
}

View File

@ -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; }
}
}

View File

@ -0,0 +1,7 @@
namespace IdentityServer.Domain.Models.Config
{
public class RestrictionsConfig
{
public int MaxFailedLoginAttempts { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace IdentityServer.Domain.Models.Config
{
public class TokenConfig
{
public int ValidityInMinutes { get; set; }
}
}

View File

@ -8,5 +8,6 @@ namespace IdentityServer.Domain.Repositories
{
Task<AppUser> GetUser(string userName, string password);
Task UpdateUserAfterAuthentication(AppUser user, Token token);
Task<UserToken[]> GetActiveTokens();
}
}

View File

@ -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
</Content>
</Note>
</ReleaseNotes>