New token structure and generation mechanism

master
Tudor Stanciu 2021-11-12 01:37:10 +02:00
parent 4f30ab0c98
commit 86a379bf17
24 changed files with 218 additions and 74 deletions

View File

@ -9,7 +9,7 @@ using System.Threading.Tasks;
namespace IdentityServer.Application.CommandHandlers namespace IdentityServer.Application.CommandHandlers
{ {
public class AuthorizeTokenHandler : IRequestHandler<AuthorizeToken, User> public class AuthorizeTokenHandler : IRequestHandler<AuthorizeToken, TokenCore>
{ {
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IMapper _mapper; private readonly IMapper _mapper;
@ -22,18 +22,19 @@ namespace IdentityServer.Application.CommandHandlers
_logger = logger; _logger = logger;
} }
public async Task<User> Handle(AuthorizeToken command, CancellationToken cancellationToken) public Task<TokenCore> Handle(AuthorizeToken command, CancellationToken cancellationToken)
{ {
var appUser = await _userService.Authorize(command.Token); var tokenCore = _userService.Authorize(command.Token);
if (appUser == null) if (tokenCore == null)
{ {
_logger.LogDebug($"Authorization failed for token '{command.Token}'."); _logger.LogDebug($"Authorization failed for token '{command.Token}'.");
return null; return null;
} }
_logger.LogDebug($"Authorization succeeded for token '{command.Token}'."); _logger.LogDebug($"Authorization succeeded for token '{command.Token}'.");
var user = _mapper.Map<User>(appUser); var tokenCoreResult = _mapper.Map<TokenCore>(tokenCore);
return user;
return Task.FromResult(tokenCoreResult);
} }
} }
} }

View File

@ -3,7 +3,7 @@ using NDB.Application.DataContracts;
namespace IdentityServer.Application.Commands namespace IdentityServer.Application.Commands
{ {
public class AuthorizeToken : Command<User> public class AuthorizeToken : Command<TokenCore>
{ {
public string Token { get; set; } public string Token { get; set; }
} }

View File

@ -9,6 +9,7 @@ namespace IdentityServer.Application
public static void AddApplicationServices(this IServiceCollection services) public static void AddApplicationServices(this IServiceCollection services)
{ {
services.AddStores(); services.AddStores();
services.AddSingleton<ITokenService, TokenService>();
services.AddScoped<IUserService, UserService>(); services.AddScoped<IUserService, UserService>();
} }

View File

@ -12,6 +12,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" />
<PackageReference Include="NDB.Application.DataContracts" Version="$(NDBApplicationPackageVersion)" /> <PackageReference Include="NDB.Application.DataContracts" Version="$(NDBApplicationPackageVersion)" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,5 +1,6 @@
using AutoMapper; using AutoMapper;
using IdentityServer.Domain.Entities; using IdentityServer.Domain.Entities;
using System.Collections.Generic;
using dto = IdentityServer.PublishedLanguage.Dto; using dto = IdentityServer.PublishedLanguage.Dto;
using models = IdentityServer.Domain.Models; using models = IdentityServer.Domain.Models;
@ -10,7 +11,21 @@ namespace IdentityServer.Application.Mappings
public MappingProfile() public MappingProfile()
{ {
CreateMap<models.Token, dto.Token>(); CreateMap<models.Token, dto.Token>();
CreateMap<AppUser, dto.User>(); CreateMap<models.TokenCore, dto.TokenCore>();
CreateMap<AppUser, dto.TokenCore>()
.ForMember(z => z.Claims, src => src.MapFrom(z => ComposeClaims(z.Claims)));
}
private Dictionary<string, string> ComposeClaims(ICollection<UserClaim> claims)
{
if (claims == null)
return null;
var result = new Dictionary<string, string>();
foreach (var claim in claims)
result.Add(claim.ClaimKey, claim.ClaimValue);
return result;
} }
} }
} }

View File

@ -0,0 +1,11 @@
using IdentityServer.Domain.Entities;
using IdentityServer.Domain.Models;
namespace IdentityServer.Application.Services
{
internal interface ITokenService
{
string GenerateTokenRaw(AppUser user);
TokenCore ExtractTokenCore(string tokenRaw);
}
}

View File

@ -1,5 +1,4 @@
using IdentityServer.Domain.Entities; using IdentityServer.Domain.Models;
using IdentityServer.Domain.Models;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace IdentityServer.Application.Services namespace IdentityServer.Application.Services
@ -7,6 +6,6 @@ namespace IdentityServer.Application.Services
public interface IUserService public interface IUserService
{ {
Task<Token> Authenticate(string userName, string password); Task<Token> Authenticate(string userName, string password);
Task<AppUser> Authorize(string token); TokenCore Authorize(string token);
} }
} }

View File

@ -0,0 +1,65 @@
using AutoMapper;
using IdentityServer.Domain.Entities;
using IdentityServer.Domain.Models;
using Newtonsoft.Json;
using System;
using System.Text;
using System.Text.RegularExpressions;
namespace IdentityServer.Application.Services
{
internal class TokenService : ITokenService
{
private readonly IMapper _mapper;
public TokenService(IMapper mapper)
{
_mapper = mapper;
}
public string GenerateTokenRaw(AppUser user)
{
var tokenCore = GenerateToken(user);
var tokenCoreString = JsonConvert.SerializeObject(tokenCore);
var tokenCoreBytes = Encoding.UTF8.GetBytes(tokenCoreString);
var tokenRaw = Convert.ToBase64String(tokenCoreBytes);
return tokenRaw;
}
private TokenCore GenerateToken(AppUser user)
{
var tokenCore = _mapper.Map<TokenCore>(user);
tokenCore.LockStamp = Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", "");
return tokenCore;
}
public TokenCore ExtractTokenCore(string tokenRaw)
{
var valid = ValidateTokenRaw(tokenRaw);
if (!valid)
return null;
var tokenCoreBytes = Convert.FromBase64String(tokenRaw);
var tokenCoreString = Encoding.UTF8.GetString(tokenCoreBytes);
var tokenCore = JsonConvert.DeserializeObject<TokenCore>(tokenCoreString);
return tokenCore;
}
private bool ValidateTokenRaw(string tokenRaw)
{
if (string.IsNullOrWhiteSpace(tokenRaw))
return false;
if (!StringIsBase64(tokenRaw))
return false;
return true;
}
private bool StringIsBase64(string str)
{
str = str.Trim();
return (str.Length % 4 == 0) && Regex.IsMatch(str, @"^[a-zA-Z0-9+/]*={0,3}$", RegexOptions.None);
}
}
}

View File

@ -1,5 +1,4 @@
using IdentityServer.Application.Stores; using IdentityServer.Application.Stores;
using IdentityServer.Domain.Entities;
using IdentityServer.Domain.Models; using IdentityServer.Domain.Models;
using IdentityServer.Domain.Repositories; using IdentityServer.Domain.Repositories;
using System; using System;
@ -7,15 +6,17 @@ using System.Threading.Tasks;
namespace IdentityServer.Application.Services namespace IdentityServer.Application.Services
{ {
public class UserService : IUserService internal class UserService : IUserService
{ {
private readonly ISecurityStore _securityStore; private readonly ISecurityStore _securityStore;
private readonly IIdentityRepository _identityRepository; private readonly IIdentityRepository _identityRepository;
private readonly ITokenService _tokenService;
public UserService(ISecurityStore securityStore, IIdentityRepository identityRepository) public UserService(ISecurityStore securityStore, IIdentityRepository identityRepository, ITokenService tokenService)
{ {
_securityStore = securityStore; _securityStore = securityStore;
_identityRepository = identityRepository; _identityRepository = identityRepository;
_tokenService = tokenService;
} }
public async Task<Token> Authenticate(string userName, string password) public async Task<Token> Authenticate(string userName, string password)
@ -24,8 +25,7 @@ namespace IdentityServer.Application.Services
if (user == null) if (user == null)
return null; return null;
var tokenRaw = $"{Guid.NewGuid()}-{Guid.NewGuid()}-{user.UserId}"; var tokenRaw = _tokenService.GenerateTokenRaw(user);
var currentDate = DateTime.Now; var currentDate = DateTime.Now;
var token = new Token() { Raw = tokenRaw, ValidFrom = currentDate, ValidUntil = currentDate.AddMonths(12) }; var token = new Token() { Raw = tokenRaw, ValidFrom = currentDate, ValidUntil = currentDate.AddMonths(12) };
_securityStore.SetToken(token, user.UserId); _securityStore.SetToken(token, user.UserId);
@ -33,16 +33,13 @@ namespace IdentityServer.Application.Services
return token; return token;
} }
public async Task<AppUser> Authorize(string token) public TokenCore Authorize(string token)
{ {
var tokenValidation = _securityStore.ValidateToken(token); var tokenCore = _securityStore.ValidateAndGetTokenCore(token);
if (tokenValidation.Success) if (tokenCore == null)
{
var user = await _identityRepository.GetAppUser(tokenValidation.UserId);
return user;
}
return null; return null;
return tokenCore;
} }
} }
} }

View File

@ -2,9 +2,9 @@
namespace IdentityServer.Application.Stores namespace IdentityServer.Application.Stores
{ {
public interface ISecurityStore internal interface ISecurityStore
{ {
void SetToken(Token token, int userId); void SetToken(Token token, int userId);
TokenValidation ValidateToken(string token); TokenCore ValidateAndGetTokenCore(string token);
} }
} }

View File

@ -1,13 +1,20 @@
using IdentityServer.Domain.Models; using IdentityServer.Application.Services;
using System; using IdentityServer.Domain.Models;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace IdentityServer.Application.Stores namespace IdentityServer.Application.Stores
{ {
public class SecurityStore : ISecurityStore internal class SecurityStore : ISecurityStore
{ {
private readonly ITokenService _tokenService;
public SecurityStore(ITokenService tokenService)
{
_tokenService = tokenService;
}
private ConcurrentDictionary<int, List<Token>> Tokens { get; } private ConcurrentDictionary<int, List<Token>> Tokens { get; }
public SecurityStore() public SecurityStore()
@ -25,30 +32,21 @@ namespace IdentityServer.Application.Stores
Tokens.TryAdd(userId, new List<Token>() { token }); Tokens.TryAdd(userId, new List<Token>() { token });
} }
public TokenValidation ValidateToken(string token) public TokenCore ValidateAndGetTokenCore(string token)
{ {
var lastIndexOfSeparator = token.LastIndexOf('-'); var tokenCore = _tokenService.ExtractTokenCore(token);
if (lastIndexOfSeparator == -1) if (tokenCore == null)
return InvalidToken; return null;
var indexOfNextCharacterAfterSeparator = lastIndexOfSeparator + 1;
var userIdString = token.Substring(indexOfNextCharacterAfterSeparator, token.Length - indexOfNextCharacterAfterSeparator);
if (!int.TryParse(userIdString, out int userId))
return InvalidToken;
var registered = Tokens.TryGetValue(userId, out List<Token> list);
var registered = Tokens.TryGetValue(tokenCore.UserId, out List<Token> list);
if (!registered) if (!registered)
return InvalidToken; return null;
var valid = list.FirstOrDefault(z => z.Raw == token); var valid = list.FirstOrDefault(z => z.Raw == token);
if (valid != null) if (valid == null)
return new TokenValidation() { Success = true, UserId = userId }; return null;
return InvalidToken; return tokenCore;
} }
private TokenValidation InvalidToken => new TokenValidation() { Success = false };
} }
} }

View File

@ -6,7 +6,7 @@ namespace IdentityServer.Domain.Data.DbContexts
{ {
public class IdentityDbContext : DbContext public class IdentityDbContext : DbContext
{ {
public DbSet<AppUser> AppUsers { get; set; } public DbSet<AppUser> Users { get; set; }
public IdentityDbContext(DbContextOptions<IdentityDbContext> options) public IdentityDbContext(DbContextOptions<IdentityDbContext> options)
: base(options) : base(options)
@ -19,7 +19,9 @@ namespace IdentityServer.Domain.Data.DbContexts
{ {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfiguration(new UserStatusConfiguration());
modelBuilder.ApplyConfiguration(new AppUserConfiguration()); modelBuilder.ApplyConfiguration(new AppUserConfiguration());
modelBuilder.ApplyConfiguration(new UserClaimConfiguration());
} }
} }
} }

View File

@ -11,6 +11,7 @@ namespace IdentityServer.Domain.Data.EntityTypeConfiguration
builder.ToTable("AppUser").HasKey(key => key.UserId); builder.ToTable("AppUser").HasKey(key => key.UserId);
builder.Property(z => z.UserId).ValueGeneratedOnAdd(); builder.Property(z => z.UserId).ValueGeneratedOnAdd();
builder.HasOne(z => z.Status).WithMany().HasForeignKey(z => z.StatusId); builder.HasOne(z => z.Status).WithMany().HasForeignKey(z => z.StatusId);
builder.HasMany(z => z.Claims).WithOne().HasForeignKey(z => z.UserId);
} }
} }
} }

View File

@ -0,0 +1,15 @@
using IdentityServer.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace IdentityServer.Domain.Data.EntityTypeConfiguration
{
class UserClaimConfiguration : IEntityTypeConfiguration<UserClaim>
{
public void Configure(EntityTypeBuilder<UserClaim> builder)
{
builder.ToTable("UserStatus").HasKey(z => z.ClaimId);
builder.Property(z => z.ClaimId).ValueGeneratedOnAdd();
}
}
}

View File

@ -15,14 +15,12 @@ namespace IdentityServer.Domain.Data.Repositories
_dbContext = dbContext; _dbContext = dbContext;
} }
public Task<AppUser> GetAppUser(int userId)
{
return _dbContext.AppUsers.FirstOrDefaultAsync(z => z.UserId == userId);
}
public Task<AppUser> GetAppUser(string userName, string password) public Task<AppUser> GetAppUser(string userName, string password)
{ {
return _dbContext.AppUsers.FirstOrDefaultAsync(z => z.UserName == userName && z.Password == password); return _dbContext.Users
.Include(z => z.Status)
.Include(z => z.Claims)
.FirstOrDefaultAsync(z => z.UserName == userName && z.Password == password);
} }
} }
} }

View File

@ -10,13 +10,13 @@ begin
create table AppUser create table AppUser
( (
UserId int identity(0, 1) constraint PK_AppUser primary key, UserId int identity(0, 1) constraint PK_AppUser primary key,
UserName varchar(100) not null, UserName varchar(100) not null constraint UQ_AppUser_UserName unique,
[Password] varchar(100) not null, [Password] varchar(100) not null,
FirstName varchar(100), FirstName varchar(100),
LastName varchar(100), LastName varchar(100),
Email varchar(100), Email varchar(100) constraint UQ_AppUser_Email unique,
ProfilePictureUrl varchar(200), ProfilePictureUrl varchar(200),
SecurityStamp varchar(200), SecurityStamp varchar(200) constraint UQ_AppUser_SecurityStamp unique,
StatusId int constraint FK_AppUser_UserStatus references UserStatus(StatusId), StatusId int constraint FK_AppUser_UserStatus references UserStatus(StatusId),
CreationDate datetime constraint DF_AppUser_CreationDate default getdate(), CreationDate datetime constraint DF_AppUser_CreationDate default getdate(),
FailedLoginAttempts int, FailedLoginAttempts int,

View File

@ -0,0 +1,11 @@
if not exists (select top 1 1 from sys.objects where name = 'UserClaim' and type = 'U')
begin
create table UserClaim
(
ClaimId int identity(1, 1) constraint PK_UserClaim primary key,
UserId int constraint FK_UserClaim_AppUser foreign key references AppUser(UserId),
ClaimKey varchar(50) not null,
ClaimValue varchar(300) not null
)
end
go

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
namespace IdentityServer.Domain.Entities namespace IdentityServer.Domain.Entities
{ {
@ -18,5 +19,6 @@ namespace IdentityServer.Domain.Entities
public DateTime LastLoginDate { get; set; } public DateTime LastLoginDate { get; set; }
public DateTime PasswordChangeDate { get; set; } public DateTime PasswordChangeDate { get; set; }
public UserStatus Status { get; set; } public UserStatus Status { get; set; }
public ICollection<UserClaim> Claims { get; set; }
} }
} }

View File

@ -0,0 +1,10 @@
namespace IdentityServer.Domain.Entities
{
public class UserClaim
{
public int ClaimId { get; set; }
public int UserId { get; set; }
public string ClaimKey { get; set; }
public string ClaimValue { get; set; }
}
}

View File

@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace IdentityServer.Domain.Models
{
public class TokenCore
{
public int UserId { get; set; }
public string UserName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string ProfilePictureUrl { get; set; }
public string SecurityStamp { get; set; }
public string LockStamp { get; set; }
public Dictionary<string, string> Claims { get; set; }
}
}

View File

@ -1,8 +0,0 @@
namespace IdentityServer.Domain.Models
{
public class TokenValidation
{
public bool Success { get; set; }
public int UserId { get; set; }
}
}

View File

@ -5,7 +5,6 @@ namespace IdentityServer.Domain.Repositories
{ {
public interface IIdentityRepository public interface IIdentityRepository
{ {
Task<AppUser> GetAppUser(int userId);
Task<AppUser> GetAppUser(string userName, string password); Task<AppUser> GetAppUser(string userName, string password);
} }
} }

View File

@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace IdentityServer.PublishedLanguage.Dto
{
public class TokenCore
{
public int UserId { get; set; }
public string UserName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string ProfilePictureUrl { get; set; }
public string SecurityStamp { get; set; }
public string LockStamp { get; set; }
public Dictionary<string, string> Claims { get; set; }
}
}

View File

@ -1,8 +0,0 @@
namespace IdentityServer.PublishedLanguage.Dto
{
public class User
{
public int UserId { get; set; }
public string UserName { get; set; }
}
}