From fdb08acd217ba33792ee131d1db561979598b3d1 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sat, 8 Apr 2023 14:55:36 +0000 Subject: [PATCH] Merged PR 78: Added roles and groups in authorization result Added roles and groups in authorization result --- Directory.Build.props | 2 +- ReleaseNotes.xml | 11 +++ .../Extensions/EntityExtensions.cs | 2 +- .../Mappings/MappingProfile.cs | 5 -- .../Services/Abstractions/ITokenService.cs | 2 - .../Services/BehaviorService.cs | 7 +- .../Services/TokenService.cs | 61 +++-------------- .../Services/UserService.cs | 2 +- .../Tuitio.Application.csproj | 3 +- .../Repositories/UserRepository.cs | 2 + .../Helpers/DataValidationHelper.cs | 15 +++++ src/Tuitio.Domain/Models/Token.cs | 67 ++++++++++++++++++- src/Tuitio.Domain/Tuitio.Domain.csproj | 5 +- .../Dto/ResultRecords.cs | 4 +- src/Tuitio.PublishedLanguage/ReleaseNotes.txt | 5 +- .../Tuitio.PublishedLanguage.csproj | 2 +- src/Tuitio.Wrapper/ReleaseNotes.txt | 5 +- src/Tuitio.Wrapper/Tuitio.Wrapper.csproj | 2 +- .../CommandHandlerTests.cs | 2 + .../Fixtures/DependencyInjectionFixture.cs | 17 ++++- .../TokenServiceTests.cs | 34 ++++++++-- .../TokenStoreTests.cs | 12 +++- 22 files changed, 182 insertions(+), 85 deletions(-) create mode 100644 src/Tuitio.Domain/Helpers/DataValidationHelper.cs diff --git a/Directory.Build.props b/Directory.Build.props index 375016d..15d41d2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 2.4.1 + 2.4.2 Tudor Stanciu STA Tuitio diff --git a/ReleaseNotes.xml b/ReleaseNotes.xml index f62bf29..f37af3a 100644 --- a/ReleaseNotes.xml +++ b/ReleaseNotes.xml @@ -103,4 +103,15 @@ ◾ The authentication handler has been updated to skip the token validation if the method from controller is marked with [AllowAnonymous] attribute. + + 2.4.2 + 2023-04-08 01:48 + + 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. + + \ No newline at end of file diff --git a/src/Tuitio.Application/Extensions/EntityExtensions.cs b/src/Tuitio.Application/Extensions/EntityExtensions.cs index 0e28c12..8ff3619 100644 --- a/src/Tuitio.Application/Extensions/EntityExtensions.cs +++ b/src/Tuitio.Application/Extensions/EntityExtensions.cs @@ -8,7 +8,7 @@ namespace Tuitio.Application.Extensions { internal static class EntityExtensions { - public static UserRole[] GetUserRoles(this AppUser user) + public static IEnumerable GetUserRoles(this AppUser user) { var roles = new List(); diff --git a/src/Tuitio.Application/Mappings/MappingProfile.cs b/src/Tuitio.Application/Mappings/MappingProfile.cs index c2fd52d..33d7f89 100644 --- a/src/Tuitio.Application/Mappings/MappingProfile.cs +++ b/src/Tuitio.Application/Mappings/MappingProfile.cs @@ -1,8 +1,6 @@ // Copyright (c) 2020 Tudor Stanciu using AutoMapper; -using Tuitio.Application.Extensions; -using Tuitio.Domain.Entities; using dto = Tuitio.PublishedLanguage.Dto; using models = Tuitio.Domain.Models; @@ -13,9 +11,6 @@ namespace Tuitio.Application.Mappings public MappingProfile() { CreateMap(); - CreateMap() - .ForMember(z => z.Claims, src => src.MapFrom(z => z.Claims.ToDictionary())); - CreateMap(); } } diff --git a/src/Tuitio.Application/Services/Abstractions/ITokenService.cs b/src/Tuitio.Application/Services/Abstractions/ITokenService.cs index c9c775d..65b7ef9 100644 --- a/src/Tuitio.Application/Services/Abstractions/ITokenService.cs +++ b/src/Tuitio.Application/Services/Abstractions/ITokenService.cs @@ -8,7 +8,5 @@ namespace Tuitio.Application.Services.Abstractions internal interface ITokenService { Token GenerateToken(AppUser user); - string GenerateTokenRaw(Token token); - Token ExtractToken(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 74616f1..d9863d1 100644 --- a/src/Tuitio.Application/Services/BehaviorService.cs +++ b/src/Tuitio.Application/Services/BehaviorService.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Tuitio.Application.Services.Abstractions; using Tuitio.Application.Stores; using Tuitio.Domain.Entities; +using Tuitio.Domain.Models; using Tuitio.Domain.Repositories; namespace Tuitio.Application.Services @@ -15,14 +16,12 @@ namespace Tuitio.Application.Services { private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; - private readonly ITokenService _tokenService; private readonly ITokenStore _securityStore; - public BehaviorService(IServiceProvider serviceProvider, ILogger logger, ITokenService tokenService, ITokenStore securityStore) + public BehaviorService(IServiceProvider serviceProvider, ILogger logger, ITokenStore securityStore) { _serviceProvider=serviceProvider; _logger=logger; - _tokenService=tokenService; _securityStore=securityStore; } @@ -44,7 +43,7 @@ namespace Tuitio.Application.Services _logger.LogInformation($"BehaviorService: {activeTokens.Length} active tokens were found in database."); foreach (var token in activeTokens) { - var storeToken = _tokenService.ExtractToken(token.Token); + var storeToken = Token.Import(token.Token); _securityStore.Set(token.Token, storeToken); } } diff --git a/src/Tuitio.Application/Services/TokenService.cs b/src/Tuitio.Application/Services/TokenService.cs index 57ca5a8..2afb97d 100644 --- a/src/Tuitio.Application/Services/TokenService.cs +++ b/src/Tuitio.Application/Services/TokenService.cs @@ -1,10 +1,7 @@ // Copyright (c) 2020 Tudor Stanciu -using AutoMapper; -using Newtonsoft.Json; -using System; -using System.Text; -using System.Text.RegularExpressions; +using System.Linq; +using Tuitio.Application.Extensions; using Tuitio.Application.Services.Abstractions; using Tuitio.Domain.Abstractions; using Tuitio.Domain.Entities; @@ -14,63 +11,21 @@ namespace Tuitio.Application.Services { internal class TokenService : ITokenService { - private readonly IMapper _mapper; private readonly IConfigProvider _configProvider; - public TokenService(IMapper mapper, IConfigProvider configProvider) + public TokenService(IConfigProvider configProvider) { - _mapper=mapper; _configProvider=configProvider; } public Token GenerateToken(AppUser user) { - var currentDate = DateTime.UtcNow; - var validUntil = currentDate.AddMinutes(_configProvider.Token.ValidityInMinutes); - - var token = _mapper.Map(user); - token.TokenId = Guid.NewGuid(); - token.LockStamp = Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", ""); - token.CreatedAt = currentDate; - token.ExpiresIn = (validUntil - currentDate).TotalMilliseconds; + var token = new Token(_configProvider.Token.ValidityInMinutes); + var claims = user.Claims?.ToDictionary(); + var userRoles = user.GetUserRoles().Select(z => z.UserRoleCode); + var userGroups = user.UserGroups?.Select(z => z.UserGroup.UserGroupCode); + token.SetUserData(user.UserId, user.UserName, user.FirstName, user.LastName, user.Email, user.SecurityStamp, claims, userRoles, userGroups); return token; } - - public string GenerateTokenRaw(Token token) - { - var tokenString = JsonConvert.SerializeObject(token); - var tokenBytes = Encoding.UTF8.GetBytes(tokenString); - var tokenRaw = Convert.ToBase64String(tokenBytes); - return tokenRaw; - } - - public Token ExtractToken(string tokenRaw) - { - var valid = ValidateTokenRaw(tokenRaw); - if (!valid) - return null; - - var tokenBytes = Convert.FromBase64String(tokenRaw); - var tokenString = Encoding.UTF8.GetString(tokenBytes); - var token = JsonConvert.DeserializeObject(tokenString); - return token; - } - - 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/src/Tuitio.Application/Services/UserService.cs b/src/Tuitio.Application/Services/UserService.cs index 4c97fda..8a71365 100644 --- a/src/Tuitio.Application/Services/UserService.cs +++ b/src/Tuitio.Application/Services/UserService.cs @@ -43,7 +43,7 @@ namespace Tuitio.Application.Services return null; var token = _tokenService.GenerateToken(user); - var raw = _tokenService.GenerateTokenRaw(token); + var raw = token.Export(); _securityStore.Set(raw, token); await _userRepository.UpdateUserAfterLogin(user, token, raw); diff --git a/src/Tuitio.Application/Tuitio.Application.csproj b/src/Tuitio.Application/Tuitio.Application.csproj index 924dd6c..fefc3fb 100644 --- a/src/Tuitio.Application/Tuitio.Application.csproj +++ b/src/Tuitio.Application/Tuitio.Application.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -11,7 +11,6 @@ - diff --git a/src/Tuitio.Domain.Data/Repositories/UserRepository.cs b/src/Tuitio.Domain.Data/Repositories/UserRepository.cs index 31999ed..b07a189 100644 --- a/src/Tuitio.Domain.Data/Repositories/UserRepository.cs +++ b/src/Tuitio.Domain.Data/Repositories/UserRepository.cs @@ -25,6 +25,8 @@ namespace Tuitio.Domain.Data.Repositories return _dbContext.Users .Include(z => z.Status) .Include(z => z.Claims) + .Include(z => z.UserRoles).ThenInclude(z => z.UserRole) + .Include(z => z.UserGroups).ThenInclude(z => z.UserGroup).ThenInclude(z => z.GroupRoles).ThenInclude(z => z.UserRole) .FirstOrDefaultAsync(z => z.UserName == userName && z.Password == password); } diff --git a/src/Tuitio.Domain/Helpers/DataValidationHelper.cs b/src/Tuitio.Domain/Helpers/DataValidationHelper.cs new file mode 100644 index 0000000..e1b0ff6 --- /dev/null +++ b/src/Tuitio.Domain/Helpers/DataValidationHelper.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2020 Tudor Stanciu + +using System.Text.RegularExpressions; + +namespace Tuitio.Domain.Helpers +{ + internal static class DataValidationHelper + { + public static 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/src/Tuitio.Domain/Models/Token.cs b/src/Tuitio.Domain/Models/Token.cs index f2621d3..5c18317 100644 --- a/src/Tuitio.Domain/Models/Token.cs +++ b/src/Tuitio.Domain/Models/Token.cs @@ -1,7 +1,11 @@ // Copyright (c) 2020 Tudor Stanciu +using Newtonsoft.Json; using System; using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; +using Tuitio.Domain.Helpers; namespace Tuitio.Domain.Models { @@ -16,7 +20,68 @@ namespace Tuitio.Domain.Models public string SecurityStamp { get; set; } public string LockStamp { get; set; } public DateTime CreatedAt { get; set; } - public double ExpiresIn { get; set; } + 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) + { + TokenId = Guid.NewGuid(); + CreatedAt = DateTime.UtcNow; + LockStamp = Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", ""); + 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) + { + UserId = userId; + UserName = userName; + FirstName = firstName; + LastName = lastName; + Email = email; + SecurityStamp = securityStamp; + Claims = claims; + UserRoles = userRoles; + UserGroups = userGroups; + } + + public string Export() + { + var tokenString = JsonConvert.SerializeObject(this); + var tokenBytes = Encoding.UTF8.GetBytes(tokenString); + var tokenRaw = Convert.ToBase64String(tokenBytes); + return tokenRaw; + } + + public static Token Import(string tokenRaw) + { + var valid = ValidateTokenRaw(tokenRaw); + if (!valid) + return null; + + var tokenBytes = Convert.FromBase64String(tokenRaw); + var tokenString = Encoding.UTF8.GetString(tokenBytes); + var token = JsonConvert.DeserializeObject(tokenString); + return token; + } + + private static bool ValidateTokenRaw(string tokenRaw) + { + if (string.IsNullOrWhiteSpace(tokenRaw)) + return false; + + if (!DataValidationHelper.StringIsBase64(tokenRaw)) + return false; + + return true; + } } } diff --git a/src/Tuitio.Domain/Tuitio.Domain.csproj b/src/Tuitio.Domain/Tuitio.Domain.csproj index dbc1517..de8aa76 100644 --- a/src/Tuitio.Domain/Tuitio.Domain.csproj +++ b/src/Tuitio.Domain/Tuitio.Domain.csproj @@ -1,7 +1,10 @@ - + net6.0 + + + diff --git a/src/Tuitio.PublishedLanguage/Dto/ResultRecords.cs b/src/Tuitio.PublishedLanguage/Dto/ResultRecords.cs index 1cdf55d..9ebedcd 100644 --- a/src/Tuitio.PublishedLanguage/Dto/ResultRecords.cs +++ b/src/Tuitio.PublishedLanguage/Dto/ResultRecords.cs @@ -18,7 +18,9 @@ namespace Tuitio.PublishedLanguage.Dto public string SecurityStamp { get; init; } public string LockStamp { get; init; } public DateTime CreatedAt { get; init; } - public double ExpiresIn { get; init; } + public long ExpiresIn { get; init; } public Dictionary Claims { get; init; } + public string[] UserRoles { get; init; } + public string[] UserGroups { get; init; } } } diff --git a/src/Tuitio.PublishedLanguage/ReleaseNotes.txt b/src/Tuitio.PublishedLanguage/ReleaseNotes.txt index b10ecfa..6605ddb 100644 --- a/src/Tuitio.PublishedLanguage/ReleaseNotes.txt +++ b/src/Tuitio.PublishedLanguage/ReleaseNotes.txt @@ -1,4 +1,7 @@ -2.2.0 release [2023-03-27 19:20] +2.2.1 release [2023-04-08 01:48] +◾ Added user roles and groups in authorization result + +2.2.0 release [2023-03-27 19:20] ◾ Added "user-info" method in API 2.1.0 release [2023-03-07 22:17] diff --git a/src/Tuitio.PublishedLanguage/Tuitio.PublishedLanguage.csproj b/src/Tuitio.PublishedLanguage/Tuitio.PublishedLanguage.csproj index 76a6c11..8d5469a 100644 --- a/src/Tuitio.PublishedLanguage/Tuitio.PublishedLanguage.csproj +++ b/src/Tuitio.PublishedLanguage/Tuitio.PublishedLanguage.csproj @@ -7,7 +7,7 @@ https://lab.code-rove.com/gitea/tudor.stanciu/tuitio Git $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/ReleaseNotes.txt")) - 2.2.0 + 2.2.1 logo.png README.md Toodle HomeLab diff --git a/src/Tuitio.Wrapper/ReleaseNotes.txt b/src/Tuitio.Wrapper/ReleaseNotes.txt index c0717a1..66b9a72 100644 --- a/src/Tuitio.Wrapper/ReleaseNotes.txt +++ b/src/Tuitio.Wrapper/ReleaseNotes.txt @@ -1,4 +1,7 @@ -2.2.0 release [2023-03-27 19:20] +2.2.1 release [2023-04-08 01:48] +◾ Added user roles and groups in authorization result + +2.2.0 release [2023-03-27 19:20] ◾ Added "user-info" method in API 2.1.0 release [2023-03-07 22:17] diff --git a/src/Tuitio.Wrapper/Tuitio.Wrapper.csproj b/src/Tuitio.Wrapper/Tuitio.Wrapper.csproj index 406d6bd..9b0d78e 100644 --- a/src/Tuitio.Wrapper/Tuitio.Wrapper.csproj +++ b/src/Tuitio.Wrapper/Tuitio.Wrapper.csproj @@ -7,7 +7,7 @@ https://lab.code-rove.com/gitea/tudor.stanciu/tuitio Git $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/ReleaseNotes.txt")) - 2.2.0 + 2.2.1 logo.png README.md Toodle HomeLab diff --git a/test/UnitTests/Tuitio.Application.Tests/CommandHandlerTests.cs b/test/UnitTests/Tuitio.Application.Tests/CommandHandlerTests.cs index 16d3efd..9288b06 100644 --- a/test/UnitTests/Tuitio.Application.Tests/CommandHandlerTests.cs +++ b/test/UnitTests/Tuitio.Application.Tests/CommandHandlerTests.cs @@ -3,6 +3,7 @@ using MediatR; using Microsoft.Extensions.DependencyInjection; using System; +using System.Linq; using System.Threading.Tasks; using Tuitio.Application.CommandHandlers; using Tuitio.Application.Tests.Fixtures; @@ -132,6 +133,7 @@ namespace Tuitio.Application.Tests Assert.NotNull(authorizationResult.Result.LockStamp); Assert.True(authorizationResult.Result.TokenId != Guid.Empty, "Token id cannot be an empty guid."); Assert.True(authorizationResult.Result.ExpiresIn > 0, "Token expiration must be a positive number."); + Assert.True(authorizationResult.Result.UserRoles.Any(), "User must have at least one role."); } [Fact] diff --git a/test/UnitTests/Tuitio.Application.Tests/Fixtures/DependencyInjectionFixture.cs b/test/UnitTests/Tuitio.Application.Tests/Fixtures/DependencyInjectionFixture.cs index 279719e..77ab408 100644 --- a/test/UnitTests/Tuitio.Application.Tests/Fixtures/DependencyInjectionFixture.cs +++ b/test/UnitTests/Tuitio.Application.Tests/Fixtures/DependencyInjectionFixture.cs @@ -14,6 +14,7 @@ using System.IO; using System.Threading.Tasks; using Tuitio.Domain.Data; using Tuitio.Domain.Data.DbContexts; +using Tuitio.Domain.Entities; using Xunit; namespace Tuitio.Application.Tests.Fixtures @@ -136,7 +137,21 @@ namespace Tuitio.Application.Tests.Fixtures SecurityStamp = "A93650FF-1FC4-4999-BAB6-3EEB174F6892", StatusId = 1, CreationDate = DateTime.Now, - FailedLoginAttempts = 0 + FailedLoginAttempts = 0, + UserRoles = new UserXUserRole[] + { + new UserXUserRole() + { + UserId = 1, + UserRoleId = 1, + UserRole = new UserRole() + { + UserRoleId = 1, + UserRoleCode = "MOCK_ROLE", + UserRoleName = "Mock role" + } + } + } }); #endregion diff --git a/test/UnitTests/Tuitio.Application.Tests/TokenServiceTests.cs b/test/UnitTests/Tuitio.Application.Tests/TokenServiceTests.cs index 22b0f33..76223b9 100644 --- a/test/UnitTests/Tuitio.Application.Tests/TokenServiceTests.cs +++ b/test/UnitTests/Tuitio.Application.Tests/TokenServiceTests.cs @@ -2,9 +2,11 @@ using Microsoft.Extensions.DependencyInjection; using System; +using System.Text; using Tuitio.Application.Services.Abstractions; using Tuitio.Application.Tests.Fixtures; using Tuitio.Domain.Entities; +using Tuitio.Domain.Models; using Xunit; namespace Tuitio.Application.Tests @@ -24,7 +26,7 @@ namespace Tuitio.Application.Tests { var user = new AppUser() { - UserId = 0, + UserId = 1, UserName = "tuitio.test", Password = "9B8769A4A742959A2D0298C36FB70623F2DFACDA8436237DF08D8DFD5B37374C", //pass123 Email = "tuitio.test@test.test", @@ -33,7 +35,21 @@ namespace Tuitio.Application.Tests StatusId = 1, FailedLoginAttempts = 0, SecurityStamp = "A93650FF-1FC4-4999-BAB6-3EEB174F6892", - CreationDate = DateTime.Now + CreationDate = DateTime.Now, + UserRoles = new UserXUserRole[] + { + new UserXUserRole() + { + UserId = 1, + UserRoleId = 1, + UserRole = new UserRole() + { + UserRoleId = 1, + UserRoleCode = "MOCK_ROLE", + UserRoleName = "Mock role" + } + } + } }; return user; @@ -52,16 +68,22 @@ namespace Tuitio.Application.Tests // Act var token = _tokenService.GenerateToken(_userMock); - var raw = _tokenService.GenerateTokenRaw(token); - var extracted = _tokenService.ExtractToken(raw); + var raw = token.Export(); + var extracted = Token.Import(raw); // Assert Assert.NotNull(token); Assert.NotNull(raw); Assert.NotNull(extracted); - Assert.True(_userMock.UserName == extracted.UserName); - Assert.True(_userMock.SecurityStamp == extracted.SecurityStamp); + Assert.Equal(_userMock.UserName, extracted.UserName); + Assert.Equal(_userMock.FirstName, extracted.FirstName); + Assert.Equal(_userMock.LastName, extracted.LastName); + Assert.Equal(_userMock.Email, extracted.Email); + Assert.Equal(_userMock.SecurityStamp, extracted.SecurityStamp); + + var decodedTokenString = Encoding.UTF8.GetString(Convert.FromBase64String(raw)); + Assert.DoesNotContain("UserRoles", decodedTokenString); } } } diff --git a/test/UnitTests/Tuitio.Application.Tests/TokenStoreTests.cs b/test/UnitTests/Tuitio.Application.Tests/TokenStoreTests.cs index ead8ae6..2426faf 100644 --- a/test/UnitTests/Tuitio.Application.Tests/TokenStoreTests.cs +++ b/test/UnitTests/Tuitio.Application.Tests/TokenStoreTests.cs @@ -9,12 +9,20 @@ namespace Tuitio.Application.Tests { public class TokenStoreTests { + private Token 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; + } + [Fact] public void Set_ShouldSetTokenInStore() { // Arrange var key = "user001"; - var expected = new Token() { TokenId = Guid.NewGuid(), UserId = 0, UserName = "test.tuitio", CreatedAt = DateTime.Now }; + var expected = GetMockedToken(); + var store = new TokenStore(); // Act @@ -30,7 +38,7 @@ namespace Tuitio.Application.Tests { // Arrange var key = "user001"; - var mock = new Token() { TokenId = Guid.NewGuid(), UserId = 0, UserName = "test.tuitio", CreatedAt = DateTime.Now }; + var mock = GetMockedToken(); var store = new TokenStore(); // Act