From ba50e1b1869aeda7c576c960900701372f23bcc2 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Wed, 3 May 2023 16:17:45 +0000 Subject: [PATCH] Merged PR 79: Added better token persistence in the database - Store the full token in database - [2.4.4] release notes update - Better field names --- Directory.Build.props | 2 +- ReleaseNotes.xml | 33 +++++++++------ .../CommandHandlers/AccountLoginHandler.cs | 2 +- .../Mappings/MappingProfile.cs | 16 +++++++- .../Services/Abstractions/ITokenService.cs | 2 +- .../Services/Abstractions/IUserService.cs | 2 +- .../Services/BehaviorService.cs | 4 +- .../Services/TokenService.cs | 9 ++--- .../Services/UserService.cs | 20 +++++----- src/Tuitio.Application/Stores/ITokenStore.cs | 5 +-- src/Tuitio.Application/Stores/TokenStore.cs | 8 ++-- .../Repositories/UserRepository.cs | 5 ++- .../Scripts/1.0.1/02.UserToken table.sql | 1 + src/Tuitio.Domain/Entities/UserToken.cs | 1 + src/Tuitio.Domain/Models/Account/Records.cs | 2 +- src/Tuitio.Domain/Models/Token.cs | 34 ++++++++-------- src/Tuitio.Domain/Models/TokenExtension.cs | 40 +++++++++++++++++++ .../Repositories/IUserRepository.cs | 2 +- .../Authentication/AuthenticationHandler.cs | 22 +++++----- .../TokenServiceTests.cs | 2 +- .../TokenStoreTests.cs | 8 ++-- .../UserServiceTests.cs | 26 ++++++------ 22 files changed, 156 insertions(+), 90 deletions(-) create mode 100644 src/Tuitio.Domain/Models/TokenExtension.cs diff --git a/Directory.Build.props b/Directory.Build.props index 828ba73..8505121 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 2.4.3 + 2.4.4 Tudor Stanciu STA Tuitio diff --git a/ReleaseNotes.xml b/ReleaseNotes.xml index 7cf9149..bd00e3e 100644 --- a/ReleaseNotes.xml +++ b/ReleaseNotes.xml @@ -19,16 +19,16 @@ ◾ Big changes in token structure. Now the token format is base64 and contains a json with all user data like username, first name, last name, profile picture url, email address and a list of claims that can be configured from the database for each user independently. ◾ The generation and validation mechanism for the token has been rewritten to meet the new token structure. - ◾ The complexity of user information has grown a lot. All users have now besides the data from token other information such as statuses, failed login attempts, last login date, password change date and security stamp. - ◾ All tokens are persisted in the database and the active ones are reload at a server failure or in case of a restart. + ◾ The complexity of user information has grown a lot. All users have now besides the data from token other information such as statuses, failed login attempts, last login date, password change date and security stamp. + ◾ All tokens are persisted in the database and the active ones are reload at a server failure or in case of a restart. - 1.1.0 - - ◾ Upgrade all projects to .NET 5 - ◾ Upgrade packages MicrosoftExtensions, AutoMapper, EntityFramework, Netmash - + 1.1.0 + + ◾ Upgrade all projects to .NET 5 + ◾ Upgrade packages MicrosoftExtensions, AutoMapper, EntityFramework, Netmash + 1.1.1 @@ -83,7 +83,7 @@ ◾ The "user-info" method returns the data of the authenticated user. ◾ Added http context accessor and authentication handler ◾ Added user contact options - ◾ Published new versions of Tuitio's nuget packages + ◾ Published new versions of Tuitio's nuget packages @@ -109,9 +109,9 @@ Added user roles and groups in authorization result ◾ The authorization result will contain the user role and group codes. They are very useful for an application because after the token is authorized, the application can directly validate its internal permissions based on roles or groups, without calling another method to obtain this information. - ◾ In addition to these changes, some refactorings were also made. - ◾ The token "expires in" information measuring unit was changed from milliseconds to seconds. - ◾ New versions of nuget packages have been released. + ◾ In addition to these changes, some refactorings were also made. + ◾ The token "expires in" information measuring unit was changed from milliseconds to seconds. + ◾ New versions of nuget packages have been released. @@ -120,7 +120,16 @@ Added IDs for user roles and groups in authorization result ◾ Based on the development from the previous version, the authorization result has been extended and will contain both IDs and codes for the groups and roles of the authenticated user. - ◾ New versions of nuget packages have been released. + ◾ New versions of nuget packages have been released. + + + + 2.4.4 + 2023-05-03 19:02 + + Added better token persistence in the database + ◾ All tokens are persisted in the database after a login operation to recover them in case of a system crash or restart. + ◾ Expired tokens are still deleted periodically. \ No newline at end of file diff --git a/src/Tuitio.Application/CommandHandlers/AccountLoginHandler.cs b/src/Tuitio.Application/CommandHandlers/AccountLoginHandler.cs index 4d6c9df..d1bf529 100644 --- a/src/Tuitio.Application/CommandHandlers/AccountLoginHandler.cs +++ b/src/Tuitio.Application/CommandHandlers/AccountLoginHandler.cs @@ -36,7 +36,7 @@ namespace Tuitio.Application.CommandHandlers _logger.LogDebug($"Login succeeded for user '{command.UserName}'."); - var result = new AccountLoginResult(loginResult.Raw, loginResult.Token.ExpiresIn); + var result = new AccountLoginResult(loginResult.Raw, loginResult.TokenExtension.Token.ExpiresIn); return Envelope.Success(result); } } diff --git a/src/Tuitio.Application/Mappings/MappingProfile.cs b/src/Tuitio.Application/Mappings/MappingProfile.cs index a1bff2f..5b10d31 100644 --- a/src/Tuitio.Application/Mappings/MappingProfile.cs +++ b/src/Tuitio.Application/Mappings/MappingProfile.cs @@ -11,7 +11,21 @@ namespace Tuitio.Application.Mappings public MappingProfile() { CreateMap(); - CreateMap(); + + CreateMap() + .ForMember(z => z.TokenId, src => src.MapFrom(z => z.Token.TokenId)) + .ForMember(z => z.UserId, src => src.MapFrom(z => z.Token.UserId)) + .ForMember(z => z.UserName, src => src.MapFrom(z => z.Token.UserName)) + .ForMember(z => z.FirstName, src => src.MapFrom(z => z.Token.FirstName)) + .ForMember(z => z.LastName, src => src.MapFrom(z => z.Token.LastName)) + .ForMember(z => z.Email, src => src.MapFrom(z => z.Token.Email)) + .ForMember(z => z.SecurityStamp, src => src.MapFrom(z => z.Token.SecurityStamp)) + .ForMember(z => z.LockStamp, src => src.MapFrom(z => z.Token.LockStamp)) + .ForMember(z => z.CreatedAt, src => src.MapFrom(z => z.Token.CreatedAt)) + .ForMember(z => z.ExpiresIn, src => src.MapFrom(z => z.Token.ExpiresIn)) + .ForMember(z => z.Claims, src => src.MapFrom(z => z.Token.Claims)) + ; + CreateMap(); } } diff --git a/src/Tuitio.Application/Services/Abstractions/ITokenService.cs b/src/Tuitio.Application/Services/Abstractions/ITokenService.cs index 65b7ef9..7a9e8fc 100644 --- a/src/Tuitio.Application/Services/Abstractions/ITokenService.cs +++ b/src/Tuitio.Application/Services/Abstractions/ITokenService.cs @@ -7,6 +7,6 @@ namespace Tuitio.Application.Services.Abstractions { internal interface ITokenService { - Token GenerateToken(AppUser user); + TokenExtension GenerateToken(AppUser user); } } \ No newline at end of file diff --git a/src/Tuitio.Application/Services/Abstractions/IUserService.cs b/src/Tuitio.Application/Services/Abstractions/IUserService.cs index 88caf1d..06f8296 100644 --- a/src/Tuitio.Application/Services/Abstractions/IUserService.cs +++ b/src/Tuitio.Application/Services/Abstractions/IUserService.cs @@ -10,6 +10,6 @@ namespace Tuitio.Application.Services.Abstractions { Task Login(string userName, string password); Task Logout(string tokenRaw); - Token Authorize(string tokenRaw); + TokenExtension Authorize(string tokenRaw); } } \ No newline at end of file diff --git a/src/Tuitio.Application/Services/BehaviorService.cs b/src/Tuitio.Application/Services/BehaviorService.cs index d9863d1..488a88d 100644 --- a/src/Tuitio.Application/Services/BehaviorService.cs +++ b/src/Tuitio.Application/Services/BehaviorService.cs @@ -43,8 +43,8 @@ namespace Tuitio.Application.Services _logger.LogInformation($"BehaviorService: {activeTokens.Length} active tokens were found in database."); foreach (var token in activeTokens) { - var storeToken = Token.Import(token.Token); - _securityStore.Set(token.Token, storeToken); + var storeToken = TokenExtension.Deserialize(token.TokenExtension); + _securityStore.Set(token.TokenExtension, storeToken); } } } diff --git a/src/Tuitio.Application/Services/TokenService.cs b/src/Tuitio.Application/Services/TokenService.cs index 3642ac7..a1eba6d 100644 --- a/src/Tuitio.Application/Services/TokenService.cs +++ b/src/Tuitio.Application/Services/TokenService.cs @@ -18,15 +18,14 @@ namespace Tuitio.Application.Services _configProvider=configProvider; } - public Token GenerateToken(AppUser user) + public TokenExtension GenerateToken(AppUser user) { - var token = new Token(_configProvider.Token.ValidityInMinutes); var claims = user.Claims?.ToDictionary(); var userRoles = user.GetUserRoles().AsRecordIdentifiers(); var userGroups = user.UserGroups?.Select(z => z.UserGroup).AsRecordIdentifiers(); - - token.SetUserData(user.UserId, user.UserName, user.FirstName, user.LastName, user.Email, user.SecurityStamp, claims, userRoles, userGroups); - return token; + var token = Token.Initialize(_configProvider.Token.ValidityInMinutes, user.UserId, user.UserName, user.FirstName, user.LastName, user.Email, user.SecurityStamp, claims); + var extension = token.Extend(userRoles, userGroups); + return extension; } } } diff --git a/src/Tuitio.Application/Services/UserService.cs b/src/Tuitio.Application/Services/UserService.cs index 8a71365..b05b4ae 100644 --- a/src/Tuitio.Application/Services/UserService.cs +++ b/src/Tuitio.Application/Services/UserService.cs @@ -42,30 +42,30 @@ namespace Tuitio.Application.Services if (!valid) return null; - var token = _tokenService.GenerateToken(user); - var raw = token.Export(); - _securityStore.Set(raw, token); + var tokenExtension = _tokenService.GenerateToken(user); + var raw = tokenExtension.Token.Export(); + _securityStore.Set(raw, tokenExtension); - await _userRepository.UpdateUserAfterLogin(user, token, raw); + await _userRepository.UpdateUserAfterLogin(user, tokenExtension, raw); - var result = new LoginResult(token, raw); + var result = new LoginResult(tokenExtension, raw); return result; } public async Task Logout(string tokenRaw) { - var token = _securityStore.Get(tokenRaw); - if (token == null) + var tokenExtension = _securityStore.Get(tokenRaw); + if (tokenExtension == null) return null; - await _userRepository.RemoveToken(token.TokenId); + await _userRepository.RemoveToken(tokenExtension.Token.TokenId); _securityStore.Remove(tokenRaw); - var result = new LogoutResult(token.UserId, token.UserName, DateTime.UtcNow); + var result = new LogoutResult(tokenExtension.Token.UserId, tokenExtension.Token.UserName, DateTime.UtcNow); return result; } - public Token Authorize(string tokenRaw) + public TokenExtension Authorize(string tokenRaw) { var token = _securityStore.Get(tokenRaw); return token; diff --git a/src/Tuitio.Application/Stores/ITokenStore.cs b/src/Tuitio.Application/Stores/ITokenStore.cs index b4026f8..63c866b 100644 --- a/src/Tuitio.Application/Stores/ITokenStore.cs +++ b/src/Tuitio.Application/Stores/ITokenStore.cs @@ -1,14 +1,13 @@ // Copyright (c) 2020 Tudor Stanciu -using System; using Tuitio.Domain.Models; namespace Tuitio.Application.Stores { internal interface ITokenStore { - Token Get(string key); - bool Set(string key, Token token); + TokenExtension Get(string key); + bool Set(string key, TokenExtension token); void Remove(string key); } } diff --git a/src/Tuitio.Application/Stores/TokenStore.cs b/src/Tuitio.Application/Stores/TokenStore.cs index 3f44440..25f9745 100644 --- a/src/Tuitio.Application/Stores/TokenStore.cs +++ b/src/Tuitio.Application/Stores/TokenStore.cs @@ -8,14 +8,14 @@ namespace Tuitio.Application.Stores { internal class TokenStore : ITokenStore { - private ConcurrentDictionary Tokens { get; } + private ConcurrentDictionary Tokens { get; } public TokenStore() { - Tokens = new ConcurrentDictionary(); + Tokens = new ConcurrentDictionary(); } - public Token Get(string key) + public TokenExtension Get(string key) { var registered = Tokens.ContainsKey(key); if (!registered) @@ -24,7 +24,7 @@ namespace Tuitio.Application.Stores return Tokens[key]; } - public bool Set(string key, Token token) + public bool Set(string key, TokenExtension token) { var registered = Tokens.ContainsKey(key); if (registered) diff --git a/src/Tuitio.Domain.Data/Repositories/UserRepository.cs b/src/Tuitio.Domain.Data/Repositories/UserRepository.cs index c9cd0e7..cd00e22 100644 --- a/src/Tuitio.Domain.Data/Repositories/UserRepository.cs +++ b/src/Tuitio.Domain.Data/Repositories/UserRepository.cs @@ -41,13 +41,16 @@ namespace Tuitio.Domain.Data.Repositories .FirstOrDefaultAsync(z => z.UserId == userId); } - public async Task UpdateUserAfterLogin(AppUser user, Token token, string tokenRaw) + public async Task UpdateUserAfterLogin(AppUser user, TokenExtension tokenExtension, string tokenRaw) { + var token = tokenExtension.Token; + var extension = tokenExtension.Serialize(); var userToken = new UserToken() { TokenId = token.TokenId, UserId = token.UserId, Token = tokenRaw, + TokenExtension = extension, ValidFrom = token.CreatedAt, ValidUntil = token.CreatedAt.AddSeconds(token.ExpiresIn) }; diff --git a/src/Tuitio.Domain.Data/Scripts/1.0.1/02.UserToken table.sql b/src/Tuitio.Domain.Data/Scripts/1.0.1/02.UserToken table.sql index 396e93a..38689d5 100644 --- a/src/Tuitio.Domain.Data/Scripts/1.0.1/02.UserToken table.sql +++ b/src/Tuitio.Domain.Data/Scripts/1.0.1/02.UserToken table.sql @@ -5,6 +5,7 @@ begin TokenId uniqueidentifier constraint PK_Token primary key, UserId int not null constraint FK_Token_AppUser foreign key references AppUser(UserId), Token varchar(1000) not null, + TokenExtension varchar(2000) not null, ValidFrom datetime not null, ValidUntil datetime not null ) diff --git a/src/Tuitio.Domain/Entities/UserToken.cs b/src/Tuitio.Domain/Entities/UserToken.cs index 9744ee5..60353a7 100644 --- a/src/Tuitio.Domain/Entities/UserToken.cs +++ b/src/Tuitio.Domain/Entities/UserToken.cs @@ -9,6 +9,7 @@ namespace Tuitio.Domain.Entities public Guid TokenId { get; set; } public int UserId { get; set; } public string Token { get; set; } + public string TokenExtension { get; set; } public DateTime ValidFrom { get; set; } public DateTime ValidUntil { get; set; } } diff --git a/src/Tuitio.Domain/Models/Account/Records.cs b/src/Tuitio.Domain/Models/Account/Records.cs index 731661c..3cb5627 100644 --- a/src/Tuitio.Domain/Models/Account/Records.cs +++ b/src/Tuitio.Domain/Models/Account/Records.cs @@ -4,6 +4,6 @@ using System; namespace Tuitio.Domain.Models.Account { - public record LoginResult(Token Token, string Raw); + public record LoginResult(TokenExtension TokenExtension, string Raw); public record LogoutResult(int UserId, string UserName, DateTime LogoutDate); } diff --git a/src/Tuitio.Domain/Models/Token.cs b/src/Tuitio.Domain/Models/Token.cs index 5e11d0c..c30b930 100644 --- a/src/Tuitio.Domain/Models/Token.cs +++ b/src/Tuitio.Domain/Models/Token.cs @@ -23,16 +23,10 @@ namespace Tuitio.Domain.Models public long ExpiresIn { get; set; } public Dictionary Claims { get; set; } - [JsonIgnore] - public IEnumerable UserRoles { get; set; } - - [JsonIgnore] - public IEnumerable UserGroups { get; set; } - [Obsolete("This constructor is only used for deserialization and should not be used for any other purpose.")] public Token() { } - public Token(int validityInMinutes) + private Token(int validityInMinutes) { TokenId = Guid.NewGuid(); CreatedAt = DateTime.UtcNow; @@ -40,17 +34,20 @@ namespace Tuitio.Domain.Models ExpiresIn = validityInMinutes * 60; // seconds } - public void SetUserData(int userId, string userName, string firstName, string lastName, string email, string securityStamp, Dictionary claims, IEnumerable userRoles, IEnumerable userGroups) + public static Token Initialize(int validityInMinutes, int userId, string userName, string firstName, string lastName, string email, string securityStamp, Dictionary claims) { - UserId = userId; - UserName = userName; - FirstName = firstName; - LastName = lastName; - Email = email; - SecurityStamp = securityStamp; - Claims = claims; - UserRoles = userRoles; - UserGroups = userGroups; + var token = new Token(validityInMinutes) + { + UserId = userId, + UserName = userName, + FirstName = firstName, + LastName = lastName, + Email = email, + SecurityStamp = securityStamp, + Claims = claims + }; + + return token; } public string Export() @@ -73,6 +70,9 @@ namespace Tuitio.Domain.Models return token; } + public TokenExtension Extend(IEnumerable userRoles, IEnumerable userGroups) + => TokenExtension.Create(this, userRoles, userGroups); + private static bool ValidateTokenRaw(string tokenRaw) { if (string.IsNullOrWhiteSpace(tokenRaw)) diff --git a/src/Tuitio.Domain/Models/TokenExtension.cs b/src/Tuitio.Domain/Models/TokenExtension.cs new file mode 100644 index 0000000..3a6c7a0 --- /dev/null +++ b/src/Tuitio.Domain/Models/TokenExtension.cs @@ -0,0 +1,40 @@ +// Copyright (c) 2020 Tudor Stanciu + +using Newtonsoft.Json; +using System; +using System.Collections.Generic; + +namespace Tuitio.Domain.Models +{ + public class TokenExtension + { + public Token Token { get; set; } + public IEnumerable UserRoles { get; set; } + public IEnumerable UserGroups { get; set; } + + [Obsolete("This constructor is only used for deserialization and should not be used for any other purpose.")] + public TokenExtension() { } + + private TokenExtension(Token token, IEnumerable userRoles, IEnumerable userGroups) + { + Token=token; + UserRoles=userRoles; + UserGroups=userGroups; + } + + public static TokenExtension Create(Token token, IEnumerable userRoles, IEnumerable userGroups) + => new(token, userRoles, userGroups); + + public string Serialize() + { + var text = JsonConvert.SerializeObject(this); + return text; + } + + public static TokenExtension Deserialize(string text) + { + var token = JsonConvert.DeserializeObject(text); + return token; + } + } +} diff --git a/src/Tuitio.Domain/Repositories/IUserRepository.cs b/src/Tuitio.Domain/Repositories/IUserRepository.cs index 4122059..13d72dd 100644 --- a/src/Tuitio.Domain/Repositories/IUserRepository.cs +++ b/src/Tuitio.Domain/Repositories/IUserRepository.cs @@ -11,7 +11,7 @@ namespace Tuitio.Domain.Repositories { Task GetUser(string userName, string password); Task GetFullUser(int userId); - Task UpdateUserAfterLogin(AppUser user, Token token, string tokenRaw); + Task UpdateUserAfterLogin(AppUser user, TokenExtension tokenExtension, string tokenRaw); Task GetActiveTokens(); Task RemoveToken(Guid tokenId); } diff --git a/src/Tuitio/Authentication/AuthenticationHandler.cs b/src/Tuitio/Authentication/AuthenticationHandler.cs index d54de57..d7dc5cd 100644 --- a/src/Tuitio/Authentication/AuthenticationHandler.cs +++ b/src/Tuitio/Authentication/AuthenticationHandler.cs @@ -59,26 +59,26 @@ namespace Tuitio.Authentication return null; } - private AuthenticationTicket GetAuthenticationTicket(Token result) + private AuthenticationTicket GetAuthenticationTicket(TokenExtension result) { var claimCollection = new Dictionary() { - { ClaimTypes.NameIdentifier, result.UserId.ToString() }, - { ClaimTypes.Name, result.UserName }, + { ClaimTypes.NameIdentifier, result.Token.UserId.ToString() }, + { ClaimTypes.Name, result.Token.UserName }, }; - if (result.FirstName != null) - claimCollection.Add(ClaimTypes.GivenName, result.FirstName); + if (result.Token.FirstName != null) + claimCollection.Add(ClaimTypes.GivenName, result.Token.FirstName); - if (result.LastName != null) - claimCollection.Add(ClaimTypes.Surname, result.FirstName); + if (result.Token.LastName != null) + claimCollection.Add(ClaimTypes.Surname, result.Token.FirstName); - if (result.Email != null) - claimCollection.Add(ClaimTypes.Email, result.Email); + if (result.Token.Email != null) + claimCollection.Add(ClaimTypes.Email, result.Token.Email); - if (result.Claims != null && result.Claims.Any()) + if (result.Token.Claims != null && result.Token.Claims.Any()) { - foreach (var claim in result.Claims) + foreach (var claim in result.Token.Claims) { if (claimCollection.ContainsKey(claim.Key)) { diff --git a/test/UnitTests/Tuitio.Application.Tests/TokenServiceTests.cs b/test/UnitTests/Tuitio.Application.Tests/TokenServiceTests.cs index 76223b9..3e4273a 100644 --- a/test/UnitTests/Tuitio.Application.Tests/TokenServiceTests.cs +++ b/test/UnitTests/Tuitio.Application.Tests/TokenServiceTests.cs @@ -68,7 +68,7 @@ namespace Tuitio.Application.Tests // Act var token = _tokenService.GenerateToken(_userMock); - var raw = token.Export(); + var raw = token.Token.Export(); var extracted = Token.Import(raw); // Assert diff --git a/test/UnitTests/Tuitio.Application.Tests/TokenStoreTests.cs b/test/UnitTests/Tuitio.Application.Tests/TokenStoreTests.cs index 2426faf..748475e 100644 --- a/test/UnitTests/Tuitio.Application.Tests/TokenStoreTests.cs +++ b/test/UnitTests/Tuitio.Application.Tests/TokenStoreTests.cs @@ -9,11 +9,11 @@ namespace Tuitio.Application.Tests { public class TokenStoreTests { - private Token GetMockedToken() + private TokenExtension GetMockedToken() { - var token = new Token(1); - token.SetUserData(0, "test.tuitio", "tuitio", "user", "user.tuitio@lab.com", Guid.NewGuid().ToString(), null, null, null); - return token; + var token = Token.Initialize(1, 0, "test.tuitio", "tuitio", "user", "user.tuitio@lab.com", Guid.NewGuid().ToString(), null); + var extension = token.Extend(null, null); + return extension; } [Fact] diff --git a/test/UnitTests/Tuitio.Application.Tests/UserServiceTests.cs b/test/UnitTests/Tuitio.Application.Tests/UserServiceTests.cs index 4618eb0..b8af0e2 100644 --- a/test/UnitTests/Tuitio.Application.Tests/UserServiceTests.cs +++ b/test/UnitTests/Tuitio.Application.Tests/UserServiceTests.cs @@ -68,13 +68,13 @@ namespace Tuitio.Application.Tests // Assert Assert.NotNull(result); - Assert.NotNull(result.Token); + Assert.NotNull(result.TokenExtension.Token); Assert.NotEmpty(result.Raw); - Assert.Equal(userName, result.Token.UserName); - Assert.True(result.Token.TokenId != Guid.Empty, "Token id cannot be an empty guid."); - Assert.NotEmpty(result.Token.LockStamp); - Assert.True(result.Token.ExpiresIn > 0, "Token expiration must be a positive number."); - Assert.True((DateTime.UtcNow - result.Token.CreatedAt).TotalMinutes <= 1, "Token creation date must be within the last minute."); + Assert.Equal(userName, result.TokenExtension.Token.UserName); + Assert.True(result.TokenExtension.Token.TokenId != Guid.Empty, "Token id cannot be an empty guid."); + Assert.NotEmpty(result.TokenExtension.Token.LockStamp); + Assert.True(result.TokenExtension.Token.ExpiresIn > 0, "Token expiration must be a positive number."); + Assert.True((DateTime.UtcNow - result.TokenExtension.Token.CreatedAt).TotalMinutes <= 1, "Token creation date must be within the last minute."); } [Fact] @@ -105,11 +105,11 @@ namespace Tuitio.Application.Tests var loginResult = await _userService.Login(userName, password); // Act - var userTokenFromDb = await dbContext.UserTokens.FirstOrDefaultAsync(z => z.TokenId == loginResult.Token.TokenId); + var userTokenFromDb = await dbContext.UserTokens.FirstOrDefaultAsync(z => z.TokenId == loginResult.TokenExtension.Token.TokenId); // Assert Assert.NotNull(userTokenFromDb); - Assert.True(loginResult.Token.TokenId != Guid.Empty, "Token id cannot be an empty guid."); + Assert.True(loginResult.TokenExtension.Token.TokenId != Guid.Empty, "Token id cannot be an empty guid."); Assert.True((DateTime.UtcNow - userTokenFromDb.ValidFrom).TotalMinutes <= 1, "Token valid from date must be within the last minute."); Assert.True(userTokenFromDb.ValidUntil > DateTime.UtcNow, "Token valid until date must be greater than the current date."); } @@ -141,11 +141,11 @@ namespace Tuitio.Application.Tests // Assert Assert.NotNull(result); - Assert.Equal(userName, result.UserName); - Assert.NotNull(result.SecurityStamp); - Assert.NotNull(result.LockStamp); - Assert.True(result.TokenId != Guid.Empty, "Token id cannot be an empty guid."); - Assert.True(result.ExpiresIn > 0, "Token expiration must be a positive number."); + Assert.Equal(userName, result.Token.UserName); + Assert.NotNull(result.Token.SecurityStamp); + Assert.NotNull(result.Token.LockStamp); + Assert.True(result.Token.TokenId != Guid.Empty, "Token id cannot be an empty guid."); + Assert.True(result.Token.ExpiresIn > 0, "Token expiration must be a positive number."); } [Fact]