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 AutoMapper;
using IdentityServer.Application; using IdentityServer.Application;
using IdentityServer.Application.Services.Abstractions;
using IdentityServer.Domain.Data; using IdentityServer.Domain.Data;
using MediatR; using MediatR;
using MediatR.Pipeline; using MediatR.Pipeline;
@ -76,6 +77,9 @@ namespace IdentityServer.Api
endpoints.MapControllers(); endpoints.MapControllers();
}); });
app.ConfigureSwagger("IdentityServer API"); app.ConfigureSwagger("IdentityServer API");
var behaviorService = app.ApplicationServices.GetService<IBehaviorService>();
behaviorService.FillTokenStore();
} }
} }
} }

View File

@ -10,5 +10,11 @@
"Microsoft.Hosting.Lifetime": "Information" "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;
using IdentityServer.Application.Services.Abstractions;
using IdentityServer.Application.Stores; using IdentityServer.Application.Stores;
using IdentityServer.Domain.Abstractions;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace IdentityServer.Application namespace IdentityServer.Application
@ -9,13 +11,15 @@ namespace IdentityServer.Application
public static void AddApplicationServices(this IServiceCollection services) public static void AddApplicationServices(this IServiceCollection services)
{ {
services.AddStores(); services.AddStores();
services.AddSingleton<IConfigProvider, ConfigProvider>();
services.AddSingleton<ITokenService, TokenService>(); services.AddSingleton<ITokenService, TokenService>();
services.AddScoped<IUserService, UserService>(); services.AddScoped<IUserService, UserService>();
services.AddScoped<IBehaviorService, BehaviorService>();
} }
private static void AddStores(this IServiceCollection services) 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 internal interface ITokenService
{ {
string GenerateTokenRaw(AppUser user); Token GenerateToken(AppUser user);
TokenCore ExtractTokenCore(string tokenRaw); TokenCore ExtractTokenCore(string tokenRaw);
} }
} }

View File

@ -1,4 +1,5 @@
using AutoMapper; using AutoMapper;
using IdentityServer.Domain.Abstractions;
using IdentityServer.Domain.Entities; using IdentityServer.Domain.Entities;
using IdentityServer.Domain.Models; using IdentityServer.Domain.Models;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -11,22 +12,32 @@ namespace IdentityServer.Application.Services
internal class TokenService : ITokenService internal class TokenService : ITokenService
{ {
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly IConfigProvider _configProvider;
public TokenService(IMapper mapper) public TokenService(IMapper mapper, IConfigProvider configProvider)
{ {
_mapper = mapper; _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 tokenCoreString = JsonConvert.SerializeObject(tokenCore);
var tokenCoreBytes = Encoding.UTF8.GetBytes(tokenCoreString); var tokenCoreBytes = Encoding.UTF8.GetBytes(tokenCoreString);
var tokenRaw = Convert.ToBase64String(tokenCoreBytes); var tokenRaw = Convert.ToBase64String(tokenCoreBytes);
return tokenRaw; return tokenRaw;
} }
private TokenCore GenerateToken(AppUser user) private TokenCore GenerateTokenCore(AppUser user)
{ {
var tokenCore = _mapper.Map<TokenCore>(user); var tokenCore = _mapper.Map<TokenCore>(user);
tokenCore.LockStamp = Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", ""); tokenCore.LockStamp = Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", "");

View File

@ -1,4 +1,6 @@
using IdentityServer.Application.Stores; using IdentityServer.Application.Stores;
using IdentityServer.Domain.Abstractions;
using IdentityServer.Domain.Entities;
using IdentityServer.Domain.Models; using IdentityServer.Domain.Models;
using IdentityServer.Domain.Repositories; using IdentityServer.Domain.Repositories;
using System; using System;
@ -8,26 +10,27 @@ namespace IdentityServer.Application.Services
{ {
internal class UserService : IUserService internal class UserService : IUserService
{ {
private readonly ISecurityStore _securityStore; private readonly ITokenStore _securityStore;
private readonly IIdentityRepository _identityRepository; private readonly IIdentityRepository _identityRepository;
private readonly ITokenService _tokenService; 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; _securityStore = securityStore;
_identityRepository = identityRepository; _identityRepository = identityRepository;
_tokenService = tokenService; _tokenService = tokenService;
_configProvider = configProvider;
} }
public async Task<Token> Authenticate(string userName, string password) public async Task<Token> Authenticate(string userName, string password)
{ {
var user = await _identityRepository.GetUser(userName, password); var user = await _identityRepository.GetUser(userName, password);
if (user == null) var valid = ValidateUser(user);
if (!valid)
return null; return null;
var tokenRaw = _tokenService.GenerateTokenRaw(user); var token = _tokenService.GenerateToken(user);
var currentDate = DateTime.Now;
var token = new Token() { Raw = tokenRaw, ValidFrom = currentDate, ValidUntil = currentDate.AddMonths(12) };
_securityStore.SetToken(token, user.UserId); _securityStore.SetToken(token, user.UserId);
await _identityRepository.UpdateUserAfterAuthentication(user, token); await _identityRepository.UpdateUserAfterAuthentication(user, token);
@ -42,5 +45,16 @@ namespace IdentityServer.Application.Services
return tokenCore; 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 namespace IdentityServer.Application.Stores
{ {
internal interface ISecurityStore internal interface ITokenStore
{ {
void SetToken(Token token, int userId); void SetToken(Token token, int userId);
TokenCore ValidateAndGetTokenCore(string token); TokenCore ValidateAndGetTokenCore(string token);

View File

@ -6,12 +6,12 @@ using System.Linq;
namespace IdentityServer.Application.Stores namespace IdentityServer.Application.Stores
{ {
internal class SecurityStore : ISecurityStore internal class TokenStore : ITokenStore
{ {
private readonly ITokenService _tokenService; private readonly ITokenService _tokenService;
private ConcurrentDictionary<int, List<Token>> Tokens { get; } private ConcurrentDictionary<int, List<Token>> Tokens { get; }
public SecurityStore(ITokenService tokenService) public TokenStore(ITokenService tokenService)
{ {
_tokenService = tokenService; _tokenService = tokenService;
Tokens = new ConcurrentDictionary<int, List<Token>>(); Tokens = new ConcurrentDictionary<int, List<Token>>();

View File

@ -8,8 +8,8 @@ namespace IdentityServer.Domain.Data.EntityTypeConfiguration
{ {
public void Configure(EntityTypeBuilder<UserToken> builder) public void Configure(EntityTypeBuilder<UserToken> builder)
{ {
builder.ToTable("UserToken").HasKey(z => z.Id); builder.ToTable("UserToken").HasKey(z => z.TokenId);
builder.Property(z => z.Id).ValueGeneratedOnAdd(); builder.Property(z => z.TokenId).ValueGeneratedOnAdd();
} }
} }
} }

View File

@ -4,6 +4,7 @@ using IdentityServer.Domain.Models;
using IdentityServer.Domain.Repositories; using IdentityServer.Domain.Repositories;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System; using System;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace IdentityServer.Domain.Data.Repositories namespace IdentityServer.Domain.Data.Repositories
@ -41,5 +42,13 @@ namespace IdentityServer.Domain.Data.Repositories
await _dbContext.SaveChangesAsync(); 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 begin
create table UserToken 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), UserId int not null constraint FK_Token_AppUser foreign key references AppUser(UserId),
Token varchar(1000) not null, Token varchar(1000) not null,
ValidFrom datetime not null, ValidFrom datetime not null,
ValidUntil datetime not null ValidUntil datetime not null,
Burnt bit
) )
end end
go 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 class UserToken
{ {
public int Id { get; set; } public int TokenId { get; set; }
public int UserId { get; set; } public int UserId { get; set; }
public string Token { get; set; } public string Token { get; set; }
public DateTime ValidFrom { get; set; } public DateTime ValidFrom { get; set; }
public DateTime ValidUntil { 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<AppUser> GetUser(string userName, string password);
Task UpdateUserAfterAuthentication(AppUser user, Token token); Task UpdateUserAfterAuthentication(AppUser user, Token token);
Task<UserToken[]> GetActiveTokens();
} }
} }

View File

@ -20,6 +20,7 @@
◾ Token improvements and hard changes ◾ Token improvements and hard changes
◾ Increase user information complexity ◾ Increase user information complexity
◾ New token structure and generation mechanism ◾ New token structure and generation mechanism
◾ Persist tokens and reload active ones at server restart
</Content> </Content>
</Note> </Note>
</ReleaseNotes> </ReleaseNotes>