From 86a379bf1774d7d55c08afb813e04af218d2ad6f Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Fri, 12 Nov 2021 01:37:10 +0200 Subject: [PATCH] New token structure and generation mechanism --- .../CommandHandlers/AuthorizeTokenHandler.cs | 13 ++-- .../Commands/AuthorizeToken.cs | 2 +- .../DependencyInjectionExtensions.cs | 1 + .../IdentityServer.Application.csproj | 1 + .../Mappings/MappingProfile.cs | 17 ++++- .../Services/ITokenService.cs | 11 ++++ .../Services/IUserService.cs | 5 +- .../Services/TokenService.cs | 65 +++++++++++++++++++ .../Services/UserService.cs | 23 +++---- .../Stores/ISecurityStore.cs | 4 +- .../Stores/SecurityStore.cs | 40 ++++++------ .../DbContexts/IdentityDbContext.cs | 4 +- .../AppUserConfiguration.cs | 1 + .../UserClaimConfiguration.cs | 15 +++++ .../Repositories/IdentityRepository.cs | 10 ++- .../02.New structure for AppUser table.sql | 6 +- .../Scripts/1.0.1/03.UserClaim table.sql | 11 ++++ IdentityServer.Domain/Entities/AppUser.cs | 2 + IdentityServer.Domain/Entities/UserClaim.cs | 10 +++ IdentityServer.Domain/Models/TokenCore.cs | 17 +++++ .../Models/TokenValidation.cs | 8 --- .../Repositories/IIdentityRepository.cs | 1 - .../Dto/TokenCore.cs | 17 +++++ IdentityServer.PublishedLanguage/Dto/User.cs | 8 --- 24 files changed, 218 insertions(+), 74 deletions(-) create mode 100644 IdentityServer.Application/Services/ITokenService.cs create mode 100644 IdentityServer.Application/Services/TokenService.cs create mode 100644 IdentityServer.Domain.Data/EntityTypeConfiguration/UserClaimConfiguration.cs create mode 100644 IdentityServer.Domain.Data/Scripts/1.0.1/03.UserClaim table.sql create mode 100644 IdentityServer.Domain/Entities/UserClaim.cs create mode 100644 IdentityServer.Domain/Models/TokenCore.cs delete mode 100644 IdentityServer.Domain/Models/TokenValidation.cs create mode 100644 IdentityServer.PublishedLanguage/Dto/TokenCore.cs delete mode 100644 IdentityServer.PublishedLanguage/Dto/User.cs diff --git a/IdentityServer.Application/CommandHandlers/AuthorizeTokenHandler.cs b/IdentityServer.Application/CommandHandlers/AuthorizeTokenHandler.cs index a0036fa..93d784a 100644 --- a/IdentityServer.Application/CommandHandlers/AuthorizeTokenHandler.cs +++ b/IdentityServer.Application/CommandHandlers/AuthorizeTokenHandler.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; namespace IdentityServer.Application.CommandHandlers { - public class AuthorizeTokenHandler : IRequestHandler + public class AuthorizeTokenHandler : IRequestHandler { private readonly IUserService _userService; private readonly IMapper _mapper; @@ -22,18 +22,19 @@ namespace IdentityServer.Application.CommandHandlers _logger = logger; } - public async Task Handle(AuthorizeToken command, CancellationToken cancellationToken) + public Task Handle(AuthorizeToken command, CancellationToken cancellationToken) { - var appUser = await _userService.Authorize(command.Token); - if (appUser == null) + var tokenCore = _userService.Authorize(command.Token); + if (tokenCore == null) { _logger.LogDebug($"Authorization failed for token '{command.Token}'."); return null; } _logger.LogDebug($"Authorization succeeded for token '{command.Token}'."); - var user = _mapper.Map(appUser); - return user; + var tokenCoreResult = _mapper.Map(tokenCore); + + return Task.FromResult(tokenCoreResult); } } } diff --git a/IdentityServer.Application/Commands/AuthorizeToken.cs b/IdentityServer.Application/Commands/AuthorizeToken.cs index a00e823..03dae3e 100644 --- a/IdentityServer.Application/Commands/AuthorizeToken.cs +++ b/IdentityServer.Application/Commands/AuthorizeToken.cs @@ -3,7 +3,7 @@ using NDB.Application.DataContracts; namespace IdentityServer.Application.Commands { - public class AuthorizeToken : Command + public class AuthorizeToken : Command { public string Token { get; set; } } diff --git a/IdentityServer.Application/DependencyInjectionExtensions.cs b/IdentityServer.Application/DependencyInjectionExtensions.cs index ac4cbe3..3fd4797 100644 --- a/IdentityServer.Application/DependencyInjectionExtensions.cs +++ b/IdentityServer.Application/DependencyInjectionExtensions.cs @@ -9,6 +9,7 @@ namespace IdentityServer.Application public static void AddApplicationServices(this IServiceCollection services) { services.AddStores(); + services.AddSingleton(); services.AddScoped(); } diff --git a/IdentityServer.Application/IdentityServer.Application.csproj b/IdentityServer.Application/IdentityServer.Application.csproj index 51020cf..7e87261 100644 --- a/IdentityServer.Application/IdentityServer.Application.csproj +++ b/IdentityServer.Application/IdentityServer.Application.csproj @@ -12,6 +12,7 @@ + diff --git a/IdentityServer.Application/Mappings/MappingProfile.cs b/IdentityServer.Application/Mappings/MappingProfile.cs index 9e44121..372c488 100644 --- a/IdentityServer.Application/Mappings/MappingProfile.cs +++ b/IdentityServer.Application/Mappings/MappingProfile.cs @@ -1,5 +1,6 @@ using AutoMapper; using IdentityServer.Domain.Entities; +using System.Collections.Generic; using dto = IdentityServer.PublishedLanguage.Dto; using models = IdentityServer.Domain.Models; @@ -10,7 +11,21 @@ namespace IdentityServer.Application.Mappings public MappingProfile() { CreateMap(); - CreateMap(); + CreateMap(); + CreateMap() + .ForMember(z => z.Claims, src => src.MapFrom(z => ComposeClaims(z.Claims))); + } + + private Dictionary ComposeClaims(ICollection claims) + { + if (claims == null) + return null; + + var result = new Dictionary(); + foreach (var claim in claims) + result.Add(claim.ClaimKey, claim.ClaimValue); + + return result; } } } diff --git a/IdentityServer.Application/Services/ITokenService.cs b/IdentityServer.Application/Services/ITokenService.cs new file mode 100644 index 0000000..e4e5361 --- /dev/null +++ b/IdentityServer.Application/Services/ITokenService.cs @@ -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); + } +} \ No newline at end of file diff --git a/IdentityServer.Application/Services/IUserService.cs b/IdentityServer.Application/Services/IUserService.cs index 4bf7618..70145a2 100644 --- a/IdentityServer.Application/Services/IUserService.cs +++ b/IdentityServer.Application/Services/IUserService.cs @@ -1,5 +1,4 @@ -using IdentityServer.Domain.Entities; -using IdentityServer.Domain.Models; +using IdentityServer.Domain.Models; using System.Threading.Tasks; namespace IdentityServer.Application.Services @@ -7,6 +6,6 @@ namespace IdentityServer.Application.Services public interface IUserService { Task Authenticate(string userName, string password); - Task Authorize(string token); + TokenCore Authorize(string token); } } \ No newline at end of file diff --git a/IdentityServer.Application/Services/TokenService.cs b/IdentityServer.Application/Services/TokenService.cs new file mode 100644 index 0000000..37a73ed --- /dev/null +++ b/IdentityServer.Application/Services/TokenService.cs @@ -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(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(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); + } + } +} diff --git a/IdentityServer.Application/Services/UserService.cs b/IdentityServer.Application/Services/UserService.cs index 32a9163..fd6ce72 100644 --- a/IdentityServer.Application/Services/UserService.cs +++ b/IdentityServer.Application/Services/UserService.cs @@ -1,5 +1,4 @@ using IdentityServer.Application.Stores; -using IdentityServer.Domain.Entities; using IdentityServer.Domain.Models; using IdentityServer.Domain.Repositories; using System; @@ -7,15 +6,17 @@ using System.Threading.Tasks; namespace IdentityServer.Application.Services { - public class UserService : IUserService + internal class UserService : IUserService { private readonly ISecurityStore _securityStore; private readonly IIdentityRepository _identityRepository; + private readonly ITokenService _tokenService; - public UserService(ISecurityStore securityStore, IIdentityRepository identityRepository) + public UserService(ISecurityStore securityStore, IIdentityRepository identityRepository, ITokenService tokenService) { _securityStore = securityStore; _identityRepository = identityRepository; + _tokenService = tokenService; } public async Task Authenticate(string userName, string password) @@ -24,8 +25,7 @@ namespace IdentityServer.Application.Services if (user == null) return null; - var tokenRaw = $"{Guid.NewGuid()}-{Guid.NewGuid()}-{user.UserId}"; - + var tokenRaw = _tokenService.GenerateTokenRaw(user); var currentDate = DateTime.Now; var token = new Token() { Raw = tokenRaw, ValidFrom = currentDate, ValidUntil = currentDate.AddMonths(12) }; _securityStore.SetToken(token, user.UserId); @@ -33,16 +33,13 @@ namespace IdentityServer.Application.Services return token; } - public async Task Authorize(string token) + public TokenCore Authorize(string token) { - var tokenValidation = _securityStore.ValidateToken(token); - if (tokenValidation.Success) - { - var user = await _identityRepository.GetAppUser(tokenValidation.UserId); - return user; - } + var tokenCore = _securityStore.ValidateAndGetTokenCore(token); + if (tokenCore == null) + return null; - return null; + return tokenCore; } } } diff --git a/IdentityServer.Application/Stores/ISecurityStore.cs b/IdentityServer.Application/Stores/ISecurityStore.cs index c46ef8f..a67f668 100644 --- a/IdentityServer.Application/Stores/ISecurityStore.cs +++ b/IdentityServer.Application/Stores/ISecurityStore.cs @@ -2,9 +2,9 @@ namespace IdentityServer.Application.Stores { - public interface ISecurityStore + internal interface ISecurityStore { void SetToken(Token token, int userId); - TokenValidation ValidateToken(string token); + TokenCore ValidateAndGetTokenCore(string token); } } diff --git a/IdentityServer.Application/Stores/SecurityStore.cs b/IdentityServer.Application/Stores/SecurityStore.cs index 6daba27..b653d68 100644 --- a/IdentityServer.Application/Stores/SecurityStore.cs +++ b/IdentityServer.Application/Stores/SecurityStore.cs @@ -1,13 +1,20 @@ -using IdentityServer.Domain.Models; -using System; +using IdentityServer.Application.Services; +using IdentityServer.Domain.Models; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; namespace IdentityServer.Application.Stores { - public class SecurityStore : ISecurityStore + internal class SecurityStore : ISecurityStore { + private readonly ITokenService _tokenService; + + public SecurityStore(ITokenService tokenService) + { + _tokenService = tokenService; + } + private ConcurrentDictionary> Tokens { get; } public SecurityStore() @@ -25,30 +32,21 @@ namespace IdentityServer.Application.Stores Tokens.TryAdd(userId, new List() { token }); } - public TokenValidation ValidateToken(string token) + public TokenCore ValidateAndGetTokenCore(string token) { - var lastIndexOfSeparator = token.LastIndexOf('-'); - if (lastIndexOfSeparator == -1) - return InvalidToken; - - 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 list); + var tokenCore = _tokenService.ExtractTokenCore(token); + if (tokenCore == null) + return null; + var registered = Tokens.TryGetValue(tokenCore.UserId, out List list); if (!registered) - return InvalidToken; + return null; var valid = list.FirstOrDefault(z => z.Raw == token); - if (valid != null) - return new TokenValidation() { Success = true, UserId = userId }; + if (valid == null) + return null; - return InvalidToken; + return tokenCore; } - - private TokenValidation InvalidToken => new TokenValidation() { Success = false }; } } diff --git a/IdentityServer.Domain.Data/DbContexts/IdentityDbContext.cs b/IdentityServer.Domain.Data/DbContexts/IdentityDbContext.cs index 87919c3..0f59e0b 100644 --- a/IdentityServer.Domain.Data/DbContexts/IdentityDbContext.cs +++ b/IdentityServer.Domain.Data/DbContexts/IdentityDbContext.cs @@ -6,7 +6,7 @@ namespace IdentityServer.Domain.Data.DbContexts { public class IdentityDbContext : DbContext { - public DbSet AppUsers { get; set; } + public DbSet Users { get; set; } public IdentityDbContext(DbContextOptions options) : base(options) @@ -19,7 +19,9 @@ namespace IdentityServer.Domain.Data.DbContexts { base.OnModelCreating(modelBuilder); + modelBuilder.ApplyConfiguration(new UserStatusConfiguration()); modelBuilder.ApplyConfiguration(new AppUserConfiguration()); + modelBuilder.ApplyConfiguration(new UserClaimConfiguration()); } } } diff --git a/IdentityServer.Domain.Data/EntityTypeConfiguration/AppUserConfiguration.cs b/IdentityServer.Domain.Data/EntityTypeConfiguration/AppUserConfiguration.cs index d39232c..f769238 100644 --- a/IdentityServer.Domain.Data/EntityTypeConfiguration/AppUserConfiguration.cs +++ b/IdentityServer.Domain.Data/EntityTypeConfiguration/AppUserConfiguration.cs @@ -11,6 +11,7 @@ namespace IdentityServer.Domain.Data.EntityTypeConfiguration builder.ToTable("AppUser").HasKey(key => key.UserId); builder.Property(z => z.UserId).ValueGeneratedOnAdd(); builder.HasOne(z => z.Status).WithMany().HasForeignKey(z => z.StatusId); + builder.HasMany(z => z.Claims).WithOne().HasForeignKey(z => z.UserId); } } } diff --git a/IdentityServer.Domain.Data/EntityTypeConfiguration/UserClaimConfiguration.cs b/IdentityServer.Domain.Data/EntityTypeConfiguration/UserClaimConfiguration.cs new file mode 100644 index 0000000..ae25519 --- /dev/null +++ b/IdentityServer.Domain.Data/EntityTypeConfiguration/UserClaimConfiguration.cs @@ -0,0 +1,15 @@ +using IdentityServer.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace IdentityServer.Domain.Data.EntityTypeConfiguration +{ + class UserClaimConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("UserStatus").HasKey(z => z.ClaimId); + builder.Property(z => z.ClaimId).ValueGeneratedOnAdd(); + } + } +} diff --git a/IdentityServer.Domain.Data/Repositories/IdentityRepository.cs b/IdentityServer.Domain.Data/Repositories/IdentityRepository.cs index 7a33807..38545eb 100644 --- a/IdentityServer.Domain.Data/Repositories/IdentityRepository.cs +++ b/IdentityServer.Domain.Data/Repositories/IdentityRepository.cs @@ -15,14 +15,12 @@ namespace IdentityServer.Domain.Data.Repositories _dbContext = dbContext; } - public Task GetAppUser(int userId) - { - return _dbContext.AppUsers.FirstOrDefaultAsync(z => z.UserId == userId); - } - public Task 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); } } } diff --git a/IdentityServer.Domain.Data/Scripts/1.0.1/02.New structure for AppUser table.sql b/IdentityServer.Domain.Data/Scripts/1.0.1/02.New structure for AppUser table.sql index 21931f0..aaaa9de 100644 --- a/IdentityServer.Domain.Data/Scripts/1.0.1/02.New structure for AppUser table.sql +++ b/IdentityServer.Domain.Data/Scripts/1.0.1/02.New structure for AppUser table.sql @@ -10,13 +10,13 @@ begin create table AppUser ( 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, FirstName varchar(100), LastName varchar(100), - Email varchar(100), + Email varchar(100) constraint UQ_AppUser_Email unique, ProfilePictureUrl varchar(200), - SecurityStamp varchar(200), + SecurityStamp varchar(200) constraint UQ_AppUser_SecurityStamp unique, StatusId int constraint FK_AppUser_UserStatus references UserStatus(StatusId), CreationDate datetime constraint DF_AppUser_CreationDate default getdate(), FailedLoginAttempts int, diff --git a/IdentityServer.Domain.Data/Scripts/1.0.1/03.UserClaim table.sql b/IdentityServer.Domain.Data/Scripts/1.0.1/03.UserClaim table.sql new file mode 100644 index 0000000..a7506bb --- /dev/null +++ b/IdentityServer.Domain.Data/Scripts/1.0.1/03.UserClaim table.sql @@ -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 \ No newline at end of file diff --git a/IdentityServer.Domain/Entities/AppUser.cs b/IdentityServer.Domain/Entities/AppUser.cs index bdba9fa..28c128a 100644 --- a/IdentityServer.Domain/Entities/AppUser.cs +++ b/IdentityServer.Domain/Entities/AppUser.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace IdentityServer.Domain.Entities { @@ -18,5 +19,6 @@ namespace IdentityServer.Domain.Entities public DateTime LastLoginDate { get; set; } public DateTime PasswordChangeDate { get; set; } public UserStatus Status { get; set; } + public ICollection Claims { get; set; } } } diff --git a/IdentityServer.Domain/Entities/UserClaim.cs b/IdentityServer.Domain/Entities/UserClaim.cs new file mode 100644 index 0000000..22231f7 --- /dev/null +++ b/IdentityServer.Domain/Entities/UserClaim.cs @@ -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; } + } +} diff --git a/IdentityServer.Domain/Models/TokenCore.cs b/IdentityServer.Domain/Models/TokenCore.cs new file mode 100644 index 0000000..cb17362 --- /dev/null +++ b/IdentityServer.Domain/Models/TokenCore.cs @@ -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 Claims { get; set; } + } +} diff --git a/IdentityServer.Domain/Models/TokenValidation.cs b/IdentityServer.Domain/Models/TokenValidation.cs deleted file mode 100644 index 91f0096..0000000 --- a/IdentityServer.Domain/Models/TokenValidation.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace IdentityServer.Domain.Models -{ - public class TokenValidation - { - public bool Success { get; set; } - public int UserId { get; set; } - } -} diff --git a/IdentityServer.Domain/Repositories/IIdentityRepository.cs b/IdentityServer.Domain/Repositories/IIdentityRepository.cs index ceded78..98479c2 100644 --- a/IdentityServer.Domain/Repositories/IIdentityRepository.cs +++ b/IdentityServer.Domain/Repositories/IIdentityRepository.cs @@ -5,7 +5,6 @@ namespace IdentityServer.Domain.Repositories { public interface IIdentityRepository { - Task GetAppUser(int userId); Task GetAppUser(string userName, string password); } } diff --git a/IdentityServer.PublishedLanguage/Dto/TokenCore.cs b/IdentityServer.PublishedLanguage/Dto/TokenCore.cs new file mode 100644 index 0000000..98873a9 --- /dev/null +++ b/IdentityServer.PublishedLanguage/Dto/TokenCore.cs @@ -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 Claims { get; set; } + } +} diff --git a/IdentityServer.PublishedLanguage/Dto/User.cs b/IdentityServer.PublishedLanguage/Dto/User.cs deleted file mode 100644 index 5c55b9c..0000000 --- a/IdentityServer.PublishedLanguage/Dto/User.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace IdentityServer.PublishedLanguage.Dto -{ - public class User - { - public int UserId { get; set; } - public string UserName { get; set; } - } -}