diff --git a/README.md b/README.md index 70354ec..49a845f 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # Tuitio Tuitio is a simple identity server implementation focused strictly on the needs of my home lab. -At the moment it has a simple API consisting of only two methods: -* ```/identity/authenticate``` - handles user authentication using credentials and generates an access token. -* ```/identity/authorize``` - manages the authorization process for a token, including verification of its existence, validity, and authenticity. +At the moment it has a simple API consisting of only three methods: +* ```/account/login``` - handles user authentication using credentials and generates an access token. +* ```/account/logout``` - handles user logout. +* ```/connect/authorize``` - manages the authorization process for a token, including verification of its existence, validity, and authenticity. ***Tuitio*** is a latin word that encompasses meanings such as supervision, safeguarding, defense, guard duty, and protection. diff --git a/ReleaseNotes.xml b/ReleaseNotes.xml index 48b7fb7..c0c61ed 100644 --- a/ReleaseNotes.xml +++ b/ReleaseNotes.xml @@ -8,16 +8,16 @@ A client/consumer can do only two things: - Authentication: An user name and a password are required in the request body. The request type is POST. The output is an object with the following structure: { token: { raw: "***", validFrom: "", validUntil: "" }, status: "SUCCESS" } - Authorization: The request type is also POST and and its scope is to authorize a token. The input is just the token in string format: { token: "***" } - For .NET consumers there are two nuget packages developed to facilitate the integration with this identity server: + For .NET consumers there are two nuget packages developed to facilitate the integration with this Tuitio server: - Tuitio.PublishedLanguage: It contains constants and classes for data transfer objects. - - Tuitio.Wrapper: It compose and executes all the REST requests to the identity server and offers to a consumer a simple interface with all methods. This interface can be injected with dependency injection at consumer startup with UseIdentityServices method. The only input is the server base address. + - Tuitio.Wrapper: It compose and executes all the REST requests to the Tuitio server and offers to a consumer a simple interface with all methods. This interface can be injected with dependency injection at consumer startup with UseTuitioServices method. The only input is the server base address. - The source of this nugets is public, but on my personal server: https://lab.code-rove.com/public-nuget-server/nuget 1.0.1 - ◾ Hard 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. + ◾ 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. @@ -60,4 +60,12 @@ ◾ Added README.md file + + 2.1.0 + + ◾ Tuitio refactoring + ◾ Added account logout method + ◾ Tuitio performance optimizations + + \ No newline at end of file diff --git a/src/Tuitio.Application/CommandHandlers/AccountLoginHandler.cs b/src/Tuitio.Application/CommandHandlers/AccountLoginHandler.cs new file mode 100644 index 0000000..4d6c9df --- /dev/null +++ b/src/Tuitio.Application/CommandHandlers/AccountLoginHandler.cs @@ -0,0 +1,44 @@ +// Copyright (c) 2020 Tudor Stanciu + +using MediatR; +using Microsoft.Extensions.Logging; +using System.Threading; +using System.Threading.Tasks; +using Tuitio.Application.Services.Abstractions; +using Tuitio.PublishedLanguage.Constants; +using Tuitio.PublishedLanguage.Dto; + +namespace Tuitio.Application.CommandHandlers +{ + public class AccountLoginHandler + { + public record Command(string UserName, string Password) : IRequest>; + + public class CommandHandler : IRequestHandler> + { + private readonly IUserService _userService; + private readonly ILogger _logger; + + public CommandHandler(IUserService userService, ILogger logger) + { + _userService = userService; + _logger = logger; + } + + public async Task> Handle(Command command, CancellationToken cancellationToken) + { + var loginResult = await _userService.Login(command.UserName, command.Password); + if (loginResult == null) + { + _logger.LogDebug($"Login failed for user '{command.UserName}'."); + return Envelope.Fail(EnvelopeError.BAD_CREDENTIALS); + } + + _logger.LogDebug($"Login succeeded for user '{command.UserName}'."); + + var result = new AccountLoginResult(loginResult.Raw, loginResult.Token.ExpiresIn); + return Envelope.Success(result); + } + } + } +} diff --git a/src/Tuitio.Application/CommandHandlers/AccountLogoutHandler.cs b/src/Tuitio.Application/CommandHandlers/AccountLogoutHandler.cs new file mode 100644 index 0000000..f07b672 --- /dev/null +++ b/src/Tuitio.Application/CommandHandlers/AccountLogoutHandler.cs @@ -0,0 +1,47 @@ +// Copyright (c) 2020 Tudor Stanciu + +using AutoMapper; +using MediatR; +using Microsoft.Extensions.Logging; +using System.Threading; +using System.Threading.Tasks; +using Tuitio.Application.Services.Abstractions; +using Tuitio.PublishedLanguage.Constants; +using Tuitio.PublishedLanguage.Dto; + +namespace Tuitio.Application.CommandHandlers +{ + public class AccountLogoutHandler + { + public record Command(string Token) : IRequest>; + + public class CommandHandler : IRequestHandler> + { + private readonly IUserService _userService; + private readonly IMapper _mapper; + private readonly ILogger _logger; + + public CommandHandler(IUserService userService, IMapper mapper, ILogger logger) + { + _userService = userService; + _mapper = mapper; + _logger = logger; + } + + public async Task> Handle(Command command, CancellationToken cancellationToken) + { + var logoutResult = await _userService.Logout(command.Token); + if (logoutResult == null) + { + _logger.LogDebug($"Logout failed for token '{command.Token}'."); + return Envelope.Fail(EnvelopeError.UNAUTHENTICATED); + } + + _logger.LogDebug($"Logout succeeded for user '{logoutResult.UserName}'."); + + var result = _mapper.Map(logoutResult); + return Envelope.Success(result); + } + } + } +} diff --git a/src/Tuitio.Application/CommandHandlers/AuthenticateUserHandler.cs b/src/Tuitio.Application/CommandHandlers/AuthenticateUserHandler.cs deleted file mode 100644 index 61e4b60..0000000 --- a/src/Tuitio.Application/CommandHandlers/AuthenticateUserHandler.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2020 Tudor Stanciu - -using AutoMapper; -using Tuitio.Application.Commands; -using Tuitio.Application.Services; -using Tuitio.PublishedLanguage.Constants; -using Tuitio.PublishedLanguage.Dto; -using Tuitio.PublishedLanguage.Events; -using MediatR; -using Microsoft.Extensions.Logging; -using System.Threading; -using System.Threading.Tasks; - -namespace Tuitio.Application.CommandHandlers -{ - public class AuthenticateUserHandler : IRequestHandler - { - private readonly IUserService _userService; - private readonly IMapper _mapper; - private readonly ILogger _logger; - - public AuthenticateUserHandler(IUserService userService, IMapper mapper, ILogger logger) - { - _userService = userService; - _mapper = mapper; - _logger = logger; - } - - public async Task Handle(AuthenticateUser command, CancellationToken cancellationToken) - { - var internalToken = await _userService.Authenticate(command.UserName, command.Password); - if (internalToken == null) - { - _logger.LogDebug($"Authentication failed for user '{command.UserName}'."); - return new AuthenticateUserResult() { Status = AuthenticationStatus.BAD_CREDENTIALS }; - } - - _logger.LogDebug($"Authentication succeeded for user '{command.UserName}'."); - var token = _mapper.Map(internalToken); - - return new AuthenticateUserResult() { Token = token, Status = AuthenticationStatus.SUCCESS }; - } - } -} diff --git a/src/Tuitio.Application/CommandHandlers/AuthorizationHandler.cs b/src/Tuitio.Application/CommandHandlers/AuthorizationHandler.cs new file mode 100644 index 0000000..cdb4999 --- /dev/null +++ b/src/Tuitio.Application/CommandHandlers/AuthorizationHandler.cs @@ -0,0 +1,48 @@ +// Copyright (c) 2020 Tudor Stanciu + +using AutoMapper; +using MediatR; +using Microsoft.Extensions.Logging; +using System.Threading; +using System.Threading.Tasks; +using Tuitio.Application.Services.Abstractions; +using Tuitio.PublishedLanguage.Constants; +using Tuitio.PublishedLanguage.Dto; + +namespace Tuitio.Application.CommandHandlers +{ + public class AuthorizationHandler + { + public record Command(string Token) : IRequest>; + + public class CommandHandler : IRequestHandler> + { + private readonly IUserService _userService; + private readonly IMapper _mapper; + private readonly ILogger _logger; + + public CommandHandler(IUserService userService, IMapper mapper, ILogger logger) + { + _userService = userService; + _mapper = mapper; + _logger = logger; + } + + public Task> Handle(Command command, CancellationToken cancellationToken) + { + var token = _userService.Authorize(command.Token); + if (token == null) + { + _logger.LogDebug($"Authorization failed for token '{command.Token}'."); + var result = Envelope.Fail(EnvelopeError.UNAUTHORIZED); + return Task.FromResult(result); + } + + _logger.LogDebug($"Authorization succeeded for token '{command.Token}'."); + var authorizationResult = _mapper.Map(token); + var envelope = Envelope.Success(authorizationResult); + return Task.FromResult(envelope); + } + } + } +} diff --git a/src/Tuitio.Application/CommandHandlers/AuthorizeTokenHandler.cs b/src/Tuitio.Application/CommandHandlers/AuthorizeTokenHandler.cs deleted file mode 100644 index 986307f..0000000 --- a/src/Tuitio.Application/CommandHandlers/AuthorizeTokenHandler.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2020 Tudor Stanciu - -using AutoMapper; -using Tuitio.Application.Commands; -using Tuitio.Application.Services; -using Tuitio.PublishedLanguage.Dto; -using MediatR; -using Microsoft.Extensions.Logging; -using System.Threading; -using System.Threading.Tasks; - -namespace Tuitio.Application.CommandHandlers -{ - public class AuthorizeTokenHandler : IRequestHandler - { - private readonly IUserService _userService; - private readonly IMapper _mapper; - private readonly ILogger _logger; - - public AuthorizeTokenHandler(IUserService userService, IMapper mapper, ILogger logger) - { - _userService = userService; - _mapper = mapper; - _logger = logger; - } - - public Task Handle(AuthorizeToken command, CancellationToken cancellationToken) - { - 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 tokenCoreResult = _mapper.Map(tokenCore); - - return Task.FromResult(tokenCoreResult); - } - } -} diff --git a/src/Tuitio.Application/Commands/AuthenticateUser.cs b/src/Tuitio.Application/Commands/AuthenticateUser.cs deleted file mode 100644 index 7345a7f..0000000 --- a/src/Tuitio.Application/Commands/AuthenticateUser.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2020 Tudor Stanciu - -using MediatR; -using Tuitio.PublishedLanguage.Events; - -namespace Tuitio.Application.Commands -{ - public class AuthenticateUser : IRequest - { - public string UserName { get; set; } - public string Password { get; set; } - } -} diff --git a/src/Tuitio.Application/Commands/AuthorizeToken.cs b/src/Tuitio.Application/Commands/AuthorizeToken.cs deleted file mode 100644 index 7cc0c32..0000000 --- a/src/Tuitio.Application/Commands/AuthorizeToken.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2020 Tudor Stanciu - -using MediatR; -using Tuitio.PublishedLanguage.Dto; - -namespace Tuitio.Application.Commands -{ - public class AuthorizeToken : IRequest - { - public string Token { get; set; } - } -} diff --git a/src/Tuitio.Application/DependencyInjectionExtensions.cs b/src/Tuitio.Application/DependencyInjectionExtensions.cs index 7ce0a8b..ea164f2 100644 --- a/src/Tuitio.Application/DependencyInjectionExtensions.cs +++ b/src/Tuitio.Application/DependencyInjectionExtensions.cs @@ -1,10 +1,10 @@ // Copyright (c) 2020 Tudor Stanciu +using Microsoft.Extensions.DependencyInjection; using Tuitio.Application.Services; using Tuitio.Application.Services.Abstractions; using Tuitio.Application.Stores; using Tuitio.Domain.Abstractions; -using Microsoft.Extensions.DependencyInjection; namespace Tuitio.Application { diff --git a/src/Tuitio.Application/Mappings/MappingProfile.cs b/src/Tuitio.Application/Mappings/MappingProfile.cs index 21b6efd..d1dfbf0 100644 --- a/src/Tuitio.Application/Mappings/MappingProfile.cs +++ b/src/Tuitio.Application/Mappings/MappingProfile.cs @@ -12,10 +12,11 @@ namespace Tuitio.Application.Mappings { public MappingProfile() { - CreateMap(); - CreateMap(); - CreateMap() + CreateMap(); + CreateMap() .ForMember(z => z.Claims, src => src.MapFrom(z => ComposeClaims(z.Claims))); + + CreateMap(); } private Dictionary ComposeClaims(ICollection claims) diff --git a/src/Tuitio.Application/Services/ITokenService.cs b/src/Tuitio.Application/Services/Abstractions/ITokenService.cs similarity index 57% rename from src/Tuitio.Application/Services/ITokenService.cs rename to src/Tuitio.Application/Services/Abstractions/ITokenService.cs index 6eec150..c9c775d 100644 --- a/src/Tuitio.Application/Services/ITokenService.cs +++ b/src/Tuitio.Application/Services/Abstractions/ITokenService.cs @@ -3,11 +3,12 @@ using Tuitio.Domain.Entities; using Tuitio.Domain.Models; -namespace Tuitio.Application.Services +namespace Tuitio.Application.Services.Abstractions { internal interface ITokenService { Token GenerateToken(AppUser user); - TokenCore ExtractTokenCore(string tokenRaw); + string GenerateTokenRaw(Token token); + Token ExtractToken(string tokenRaw); } } \ No newline at end of file diff --git a/src/Tuitio.Application/Services/Abstractions/IUserService.cs b/src/Tuitio.Application/Services/Abstractions/IUserService.cs new file mode 100644 index 0000000..88caf1d --- /dev/null +++ b/src/Tuitio.Application/Services/Abstractions/IUserService.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2020 Tudor Stanciu + +using System.Threading.Tasks; +using Tuitio.Domain.Models; +using Tuitio.Domain.Models.Account; + +namespace Tuitio.Application.Services.Abstractions +{ + public interface IUserService + { + Task Login(string userName, string password); + Task Logout(string tokenRaw); + Token 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 c4e5bee..74616f1 100644 --- a/src/Tuitio.Application/Services/BehaviorService.cs +++ b/src/Tuitio.Application/Services/BehaviorService.cs @@ -1,14 +1,13 @@ // Copyright (c) 2020 Tudor Stanciu -using Tuitio.Application.Services.Abstractions; -using Tuitio.Application.Stores; -using Tuitio.Domain.Entities; -using Tuitio.Domain.Models; -using Tuitio.Domain.Repositories; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; using System.Threading.Tasks; +using Tuitio.Application.Services.Abstractions; +using Tuitio.Application.Stores; +using Tuitio.Domain.Entities; +using Tuitio.Domain.Repositories; namespace Tuitio.Application.Services { @@ -16,13 +15,15 @@ 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, ITokenStore securityStore) + public BehaviorService(IServiceProvider serviceProvider, ILogger logger, ITokenService tokenService, ITokenStore securityStore) { - _serviceProvider = serviceProvider; - _logger = logger; - _securityStore = securityStore; + _serviceProvider=serviceProvider; + _logger=logger; + _tokenService=tokenService; + _securityStore=securityStore; } public void FillTokenStore() @@ -33,7 +34,7 @@ namespace Tuitio.Application.Services var activeTokens = Array.Empty(); using (var scope = _serviceProvider.CreateScope()) { - var _repository = scope.ServiceProvider.GetRequiredService(); + var _repository = scope.ServiceProvider.GetRequiredService(); activeTokens = await _repository.GetActiveTokens(); } @@ -43,8 +44,8 @@ namespace Tuitio.Application.Services _logger.LogInformation($"BehaviorService: {activeTokens.Length} active tokens were found in database."); foreach (var token in activeTokens) { - var storeToken = new Token() { Raw = token.Token, ValidFrom = token.ValidFrom, ValidUntil = token.ValidUntil }; - _securityStore.SetToken(storeToken, token.UserId); + var storeToken = _tokenService.ExtractToken(token.Token); + _securityStore.Set(token.Token, storeToken); } } } diff --git a/src/Tuitio.Application/Services/IUserService.cs b/src/Tuitio.Application/Services/IUserService.cs deleted file mode 100644 index 7b12c69..0000000 --- a/src/Tuitio.Application/Services/IUserService.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2020 Tudor Stanciu - -using Tuitio.Domain.Models; -using System.Threading.Tasks; - -namespace Tuitio.Application.Services -{ - public interface IUserService - { - Task Authenticate(string userName, string password); - TokenCore Authorize(string token); - } -} \ No newline at end of file diff --git a/src/Tuitio.Application/Services/TokenService.cs b/src/Tuitio.Application/Services/TokenService.cs index 0353639..2897ef8 100644 --- a/src/Tuitio.Application/Services/TokenService.cs +++ b/src/Tuitio.Application/Services/TokenService.cs @@ -1,13 +1,15 @@ // Copyright (c) 2020 Tudor Stanciu using AutoMapper; -using Tuitio.Domain.Abstractions; -using Tuitio.Domain.Entities; -using Tuitio.Domain.Models; using Newtonsoft.Json; using System; using System.Text; using System.Text.RegularExpressions; +using Tuitio.Application.Services.Abstractions; +using Tuitio.Application.Stores; +using Tuitio.Domain.Abstractions; +using Tuitio.Domain.Entities; +using Tuitio.Domain.Models; namespace Tuitio.Application.Services { @@ -15,47 +17,46 @@ namespace Tuitio.Application.Services { private readonly IMapper _mapper; private readonly IConfigProvider _configProvider; + private readonly ITokenStore _tokenStore; - public TokenService(IMapper mapper, IConfigProvider configProvider) + public TokenService(IMapper mapper, IConfigProvider configProvider, ITokenStore tokenStore) { - _mapper = mapper; - _configProvider = configProvider; + _mapper=mapper; + _configProvider=configProvider; + _tokenStore=tokenStore; } public Token GenerateToken(AppUser user) { - var tokenRaw = GenerateTokenRaw(user); - var currentDate = DateTime.Now; - var token = new Token() { Raw = tokenRaw, ValidFrom = currentDate, ValidUntil = currentDate.AddMinutes(_configProvider.Token.ValidityInMinutes) }; + 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; return token; } - private string GenerateTokenRaw(AppUser user) + public string GenerateTokenRaw(Token token) { - var tokenCore = GenerateTokenCore(user); - var tokenCoreString = JsonConvert.SerializeObject(tokenCore); - var tokenCoreBytes = Encoding.UTF8.GetBytes(tokenCoreString); - var tokenRaw = Convert.ToBase64String(tokenCoreBytes); + var tokenString = JsonConvert.SerializeObject(token); + var tokenBytes = Encoding.UTF8.GetBytes(tokenString); + var tokenRaw = Convert.ToBase64String(tokenBytes); return tokenRaw; } - private TokenCore GenerateTokenCore(AppUser user) - { - var tokenCore = _mapper.Map(user); - tokenCore.LockStamp = Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", ""); - return tokenCore; - } - - public TokenCore ExtractTokenCore(string tokenRaw) + public Token ExtractToken(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; + var tokenBytes = Convert.FromBase64String(tokenRaw); + var tokenString = Encoding.UTF8.GetString(tokenBytes); + var token = JsonConvert.DeserializeObject(tokenString); + return token; } private bool ValidateTokenRaw(string tokenRaw) diff --git a/src/Tuitio.Application/Services/UserService.cs b/src/Tuitio.Application/Services/UserService.cs index 24cafe3..df57c5d 100644 --- a/src/Tuitio.Application/Services/UserService.cs +++ b/src/Tuitio.Application/Services/UserService.cs @@ -1,61 +1,78 @@ // Copyright (c) 2020 Tudor Stanciu +using System; +using System.Threading.Tasks; using Tuitio.Application.Services.Abstractions; using Tuitio.Application.Stores; using Tuitio.Domain.Abstractions; using Tuitio.Domain.Entities; using Tuitio.Domain.Models; +using Tuitio.Domain.Models.Account; using Tuitio.Domain.Repositories; -using System; -using System.Threading.Tasks; namespace Tuitio.Application.Services { internal class UserService : IUserService { private readonly ITokenStore _securityStore; - private readonly IIdentityRepository _identityRepository; + private readonly IUserRepository _userRepository; private readonly ITokenService _tokenService; private readonly IConfigProvider _configProvider; private readonly IHashingService _hashingService; - public UserService(ITokenStore securityStore, IIdentityRepository identityRepository, ITokenService tokenService, IConfigProvider configProvider, IHashingService hashingService) + public UserService(ITokenStore securityStore, IUserRepository userRepository, ITokenService tokenService, IConfigProvider configProvider, IHashingService hashingService) { _securityStore=securityStore; - _identityRepository=identityRepository; + _userRepository=userRepository; _tokenService=tokenService; _configProvider=configProvider; _hashingService=hashingService; } - public async Task Authenticate(string userName, string password) + public async Task Login(string userName, string password) { var passwordHash = _hashingService.HashSha256(password); - var user = await _identityRepository.GetUser(userName, passwordHash); + var user = await _userRepository.GetUser(userName, passwordHash); + if (user == null) + return null; + var valid = ValidateUser(user); if (!valid) return null; var token = _tokenService.GenerateToken(user); - _securityStore.SetToken(token, user.UserId); - await _identityRepository.UpdateUserAfterAuthentication(user, token); + var raw = _tokenService.GenerateTokenRaw(token); + _securityStore.Set(raw, token); - return token; + await _userRepository.UpdateUserAfterLogin(user, token, raw); + + var result = new LoginResult(token, raw); + return result; } - public TokenCore Authorize(string token) + public async Task Logout(string tokenRaw) { - var tokenCore = _securityStore.ValidateAndGetTokenCore(token); - if (tokenCore == null) + var token = _securityStore.Get(tokenRaw); + if (token == null) return null; - return tokenCore; + await _userRepository.RemoveToken(token.TokenId); + _securityStore.Remove(tokenRaw); + + var result = new LogoutResult(token.UserId, token.UserName, DateTime.UtcNow); + return result; + } + + public Token Authorize(string tokenRaw) + { + var token = _securityStore.Get(tokenRaw); + return token; } private bool ValidateUser(AppUser user) { if (user == null) - return false; + throw new ArgumentNullException(nameof(user)); if (user.FailedLoginAttempts.HasValue && user.FailedLoginAttempts.Value > _configProvider.Restrictions.MaxFailedLoginAttempts) return false; diff --git a/src/Tuitio.Application/Stores/ITokenStore.cs b/src/Tuitio.Application/Stores/ITokenStore.cs index 0c4e1b9..b4026f8 100644 --- a/src/Tuitio.Application/Stores/ITokenStore.cs +++ b/src/Tuitio.Application/Stores/ITokenStore.cs @@ -1,12 +1,14 @@ // Copyright (c) 2020 Tudor Stanciu +using System; using Tuitio.Domain.Models; namespace Tuitio.Application.Stores { internal interface ITokenStore { - void SetToken(Token token, int userId); - TokenCore ValidateAndGetTokenCore(string token); + Token Get(string key); + bool Set(string key, Token token); + void Remove(string key); } } diff --git a/src/Tuitio.Application/Stores/TokenStore.cs b/src/Tuitio.Application/Stores/TokenStore.cs index c427197..3f44440 100644 --- a/src/Tuitio.Application/Stores/TokenStore.cs +++ b/src/Tuitio.Application/Stores/TokenStore.cs @@ -1,49 +1,39 @@ // Copyright (c) 2020 Tudor Stanciu -using Tuitio.Application.Services; -using Tuitio.Domain.Models; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; +using Tuitio.Domain.Models; namespace Tuitio.Application.Stores { internal class TokenStore : ITokenStore { - private readonly ITokenService _tokenService; - private ConcurrentDictionary> Tokens { get; } + private ConcurrentDictionary Tokens { get; } - public TokenStore(ITokenService tokenService) + public TokenStore() { - _tokenService = tokenService; - Tokens = new ConcurrentDictionary>(); + Tokens = new ConcurrentDictionary(); } - public void SetToken(Token token, int userId) + public Token Get(string key) { - var registered = Tokens.TryGetValue(userId, out List list); - - if (registered) - list.Add(token); - else - Tokens.TryAdd(userId, new List() { token }); - } - - public TokenCore ValidateAndGetTokenCore(string token) - { - var tokenCore = _tokenService.ExtractTokenCore(token); - if (tokenCore == null) - return null; - - var registered = Tokens.TryGetValue(tokenCore.UserId, out List list); + var registered = Tokens.ContainsKey(key); if (!registered) return null; - var valid = list.FirstOrDefault(z => z.Raw == token); - if (valid == null) - return null; - - return tokenCore; + return Tokens[key]; } + + public bool Set(string key, Token token) + { + var registered = Tokens.ContainsKey(key); + if (registered) + return false; + + Tokens.TryAdd(key, token); + return true; + } + + public void Remove(string key) => Tokens.Remove(key, out _); } } diff --git a/src/Tuitio.Domain.Data/DbContexts/IdentityDbContext.cs b/src/Tuitio.Domain.Data/DbContexts/TuitioDbContext.cs similarity index 88% rename from src/Tuitio.Domain.Data/DbContexts/IdentityDbContext.cs rename to src/Tuitio.Domain.Data/DbContexts/TuitioDbContext.cs index cf61a1a..0b5580d 100644 --- a/src/Tuitio.Domain.Data/DbContexts/IdentityDbContext.cs +++ b/src/Tuitio.Domain.Data/DbContexts/TuitioDbContext.cs @@ -6,12 +6,12 @@ using Microsoft.EntityFrameworkCore; namespace Tuitio.Domain.Data.DbContexts { - public class IdentityDbContext : DbContext + public class TuitioDbContext : DbContext { public DbSet Users { get; set; } public DbSet UserTokens { get; set; } - public IdentityDbContext(DbContextOptions options) + public TuitioDbContext(DbContextOptions options) : base(options) { base.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll; diff --git a/src/Tuitio.Domain.Data/DependencyInjectionExtensions.cs b/src/Tuitio.Domain.Data/DependencyInjectionExtensions.cs index 47a41c2..36e3dd6 100644 --- a/src/Tuitio.Domain.Data/DependencyInjectionExtensions.cs +++ b/src/Tuitio.Domain.Data/DependencyInjectionExtensions.cs @@ -13,10 +13,10 @@ namespace Tuitio.Domain.Data { public static void AddDataAccess(this IServiceCollection services) { - services.AddScoped(); + services.AddScoped(); services - .AddDbContextPool( + .AddDbContextPool( (serviceProvider, options) => { var configuration = serviceProvider.GetService(); diff --git a/src/Tuitio.Domain.Data/EntityTypeConfiguration/UserTokenConfiguration.cs b/src/Tuitio.Domain.Data/EntityTypeConfiguration/UserTokenConfiguration.cs index 4c39237..5676689 100644 --- a/src/Tuitio.Domain.Data/EntityTypeConfiguration/UserTokenConfiguration.cs +++ b/src/Tuitio.Domain.Data/EntityTypeConfiguration/UserTokenConfiguration.cs @@ -1,8 +1,8 @@ // Copyright (c) 2020 Tudor Stanciu -using Tuitio.Domain.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Tuitio.Domain.Entities; namespace Tuitio.Domain.Data.EntityTypeConfiguration { @@ -11,7 +11,6 @@ namespace Tuitio.Domain.Data.EntityTypeConfiguration public void Configure(EntityTypeBuilder builder) { builder.ToTable("UserToken").HasKey(z => z.TokenId); - builder.Property(z => z.TokenId).ValueGeneratedOnAdd(); } } } diff --git a/src/Tuitio.Domain.Data/Repositories/IdentityRepository.cs b/src/Tuitio.Domain.Data/Repositories/IdentityRepository.cs deleted file mode 100644 index 77ec773..0000000 --- a/src/Tuitio.Domain.Data/Repositories/IdentityRepository.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2020 Tudor Stanciu - -using Tuitio.Domain.Data.DbContexts; -using Tuitio.Domain.Entities; -using Tuitio.Domain.Models; -using Tuitio.Domain.Repositories; -using Microsoft.EntityFrameworkCore; -using System; -using System.Linq; -using System.Threading.Tasks; - -namespace Tuitio.Domain.Data.Repositories -{ - class IdentityRepository : IIdentityRepository - { - private readonly IdentityDbContext _dbContext; - - public IdentityRepository(IdentityDbContext dbContext) - { - _dbContext = dbContext; - } - - public Task GetUser(string userName, string password) - { - return _dbContext.Users - .Include(z => z.Status) - .Include(z => z.Claims) - .FirstOrDefaultAsync(z => z.UserName == userName && z.Password == password); - } - - public async Task UpdateUserAfterAuthentication(AppUser user, Token token) - { - var userToken = new UserToken() - { - UserId = user.UserId, - Token = token.Raw, - ValidFrom = token.ValidFrom, - ValidUntil = token.ValidUntil - }; - await _dbContext.AddAsync(userToken); - - user.LastLoginDate = DateTime.Now; - _dbContext.Update(user); - - await _dbContext.SaveChangesAsync(); - } - - public Task 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(); - } - } -} diff --git a/src/Tuitio.Domain.Data/Repositories/UserRepository.cs b/src/Tuitio.Domain.Data/Repositories/UserRepository.cs new file mode 100644 index 0000000..e11b6f6 --- /dev/null +++ b/src/Tuitio.Domain.Data/Repositories/UserRepository.cs @@ -0,0 +1,77 @@ +// Copyright (c) 2020 Tudor Stanciu + +using Microsoft.EntityFrameworkCore; +using System; +using System.Linq; +using System.Threading.Tasks; +using Tuitio.Domain.Data.DbContexts; +using Tuitio.Domain.Entities; +using Tuitio.Domain.Models; +using Tuitio.Domain.Repositories; + +namespace Tuitio.Domain.Data.Repositories +{ + class UserRepository : IUserRepository + { + private readonly TuitioDbContext _dbContext; + + public UserRepository(TuitioDbContext dbContext) + { + _dbContext = dbContext; + } + + public Task GetUser(string userName, string password) + { + return _dbContext.Users + .Include(z => z.Status) + .Include(z => z.Claims) + .FirstOrDefaultAsync(z => z.UserName == userName && z.Password == password); + } + + public async Task UpdateUserAfterLogin(AppUser user, Token token, string tokenRaw) + { + var userToken = new UserToken() + { + TokenId = token.TokenId, + UserId = token.UserId, + Token = tokenRaw, + ValidFrom = token.CreatedAt, + ValidUntil = token.CreatedAt.AddMilliseconds(token.ExpiresIn) + }; + + await _dbContext.AddAsync(userToken); + user.LastLoginDate = DateTime.UtcNow; + + await _dbContext.SaveChangesAsync(); + } + + public async Task GetActiveTokens() + { + var currentDate = DateTime.UtcNow; + + // remove expired tokens + _dbContext.UserTokens.RemoveRange(_dbContext.UserTokens.Where(z => z.ValidUntil < currentDate)); + await _dbContext.SaveChangesAsync(); + + // retrieve active tokens + var query = _dbContext.UserTokens + .Where(z => z.ValidFrom <= currentDate && z.ValidUntil >= currentDate); + + var tokens = await query.ToArrayAsync(); + return tokens; + } + + public Task RemoveToken(Guid tokenId) + { + var token = new UserToken() + { + TokenId = tokenId + }; + + _dbContext.UserTokens.Attach(token); + _dbContext.UserTokens.Remove(token); + + return _dbContext.SaveChangesAsync(); + } + } +} 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 22b953c..396e93a 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 @@ -2,11 +2,10 @@ if not exists (select top 1 1 from sys.objects where name = 'UserToken' and type begin create table UserToken ( - TokenId int identity(1, 1) constraint PK_Token primary key, + 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, ValidFrom datetime not null, - ValidUntil datetime not null, - Burnt bit + ValidUntil datetime not null ) end \ No newline at end of file diff --git a/src/Tuitio.Domain/Entities/UserToken.cs b/src/Tuitio.Domain/Entities/UserToken.cs index e2f59a4..9744ee5 100644 --- a/src/Tuitio.Domain/Entities/UserToken.cs +++ b/src/Tuitio.Domain/Entities/UserToken.cs @@ -6,11 +6,10 @@ namespace Tuitio.Domain.Entities { public class UserToken { - public int TokenId { get; set; } + public Guid TokenId { get; set; } public int UserId { get; set; } public string Token { get; set; } public DateTime ValidFrom { get; set; } public DateTime ValidUntil { get; set; } - public bool? Burnt { get; set; } } } diff --git a/src/Tuitio.Domain/Models/Account/Records.cs b/src/Tuitio.Domain/Models/Account/Records.cs new file mode 100644 index 0000000..731661c --- /dev/null +++ b/src/Tuitio.Domain/Models/Account/Records.cs @@ -0,0 +1,9 @@ +// Copyright (c) 2020 Tudor Stanciu + +using System; + +namespace Tuitio.Domain.Models.Account +{ + public record LoginResult(Token Token, 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 1bf56a2..9877562 100644 --- a/src/Tuitio.Domain/Models/Token.cs +++ b/src/Tuitio.Domain/Models/Token.cs @@ -1,13 +1,23 @@ // Copyright (c) 2020 Tudor Stanciu using System; +using System.Collections.Generic; namespace Tuitio.Domain.Models { public class Token { - public string Raw { get; set; } - public DateTime ValidFrom { get; set; } - public DateTime ValidUntil { get; set; } + public Guid TokenId { get; set; } + 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 DateTime CreatedAt { get; set; } + public double ExpiresIn { get; set; } + public Dictionary Claims { get; set; } } } diff --git a/src/Tuitio.Domain/Models/TokenCore.cs b/src/Tuitio.Domain/Models/TokenCore.cs deleted file mode 100644 index f8259c3..0000000 --- a/src/Tuitio.Domain/Models/TokenCore.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) 2020 Tudor Stanciu - -using System.Collections.Generic; - -namespace Tuitio.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/src/Tuitio.Domain/Repositories/IIdentityRepository.cs b/src/Tuitio.Domain/Repositories/IUserRepository.cs similarity index 63% rename from src/Tuitio.Domain/Repositories/IIdentityRepository.cs rename to src/Tuitio.Domain/Repositories/IUserRepository.cs index c20b145..158f629 100644 --- a/src/Tuitio.Domain/Repositories/IIdentityRepository.cs +++ b/src/Tuitio.Domain/Repositories/IUserRepository.cs @@ -3,13 +3,15 @@ using Tuitio.Domain.Entities; using Tuitio.Domain.Models; using System.Threading.Tasks; +using System; namespace Tuitio.Domain.Repositories { - public interface IIdentityRepository + public interface IUserRepository { Task GetUser(string userName, string password); - Task UpdateUserAfterAuthentication(AppUser user, Token token); + Task UpdateUserAfterLogin(AppUser user, Token token, string tokenRaw); Task GetActiveTokens(); + Task RemoveToken(Guid tokenId); } } diff --git a/src/Tuitio.PublishedLanguage/Constants/AuthenticationStatus.cs b/src/Tuitio.PublishedLanguage/Constants/AuthenticationStatus.cs deleted file mode 100644 index 2a57aed..0000000 --- a/src/Tuitio.PublishedLanguage/Constants/AuthenticationStatus.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2020 Tudor Stanciu - -namespace Tuitio.PublishedLanguage.Constants -{ - public struct AuthenticationStatus - { - public const string - SUCCESS = "SUCCESS", - BAD_CREDENTIALS = "BAD_CREDENTIALS"; - } -} diff --git a/src/Tuitio.PublishedLanguage/Constants/EnvelopeError.cs b/src/Tuitio.PublishedLanguage/Constants/EnvelopeError.cs new file mode 100644 index 0000000..40e7444 --- /dev/null +++ b/src/Tuitio.PublishedLanguage/Constants/EnvelopeError.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2020 Tudor Stanciu + +namespace Tuitio.PublishedLanguage.Constants +{ + public struct EnvelopeError + { + public const string + BAD_CREDENTIALS = "BAD_CREDENTIALS", + UNAUTHENTICATED = "UNAUTHENTICATED", + UNAUTHORIZED = "UNAUTHORIZED"; + } +} diff --git a/src/Tuitio.PublishedLanguage/Dto/Envelope.cs b/src/Tuitio.PublishedLanguage/Dto/Envelope.cs new file mode 100644 index 0000000..18ba4dd --- /dev/null +++ b/src/Tuitio.PublishedLanguage/Dto/Envelope.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2020 Tudor Stanciu + +namespace Tuitio.PublishedLanguage.Dto +{ + public class Envelope where T : class + { + public T Result { get; } + public string Error { get; } + + public Envelope(T result, string error) + { + Result=result; + Error=error; + } + + public static Envelope Success(T result) + { + return new Envelope(result, null); + } + + public static Envelope Fail(string message) + { + return new Envelope(null, message); + } + } +} diff --git a/src/Tuitio.PublishedLanguage/Dto/ResultRecords.cs b/src/Tuitio.PublishedLanguage/Dto/ResultRecords.cs new file mode 100644 index 0000000..21bcbf2 --- /dev/null +++ b/src/Tuitio.PublishedLanguage/Dto/ResultRecords.cs @@ -0,0 +1,25 @@ +// Copyright (c) 2020 Tudor Stanciu + +using System; +using System.Collections.Generic; + +namespace Tuitio.PublishedLanguage.Dto +{ + public record AccountLoginResult(string Token, double ExpiresIn); + public record AccountLogoutResult(int UserId, string UserName, DateTime LogoutDate); + public class AuthorizationResult + { + public Guid TokenId { get; init; } + public int UserId { get; init; } + public string UserName { get; init; } + public string FirstName { get; init; } + public string LastName { get; init; } + public string Email { get; init; } + public string ProfilePictureUrl { get; init; } + public string SecurityStamp { get; init; } + public string LockStamp { get; init; } + public DateTime CreatedAt { get; init; } + public double ExpiresIn { get; init; } + public Dictionary Claims { get; init; } + } +} diff --git a/src/Tuitio.PublishedLanguage/Dto/Token.cs b/src/Tuitio.PublishedLanguage/Dto/Token.cs deleted file mode 100644 index af4d362..0000000 --- a/src/Tuitio.PublishedLanguage/Dto/Token.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2020 Tudor Stanciu - -using System; - -namespace Tuitio.PublishedLanguage.Dto -{ - public class Token - { - public string Raw { get; set; } - public DateTime ValidFrom { get; set; } - public DateTime ValidUntil { get; set; } - } -} diff --git a/src/Tuitio.PublishedLanguage/Dto/TokenCore.cs b/src/Tuitio.PublishedLanguage/Dto/TokenCore.cs deleted file mode 100644 index ecd5db5..0000000 --- a/src/Tuitio.PublishedLanguage/Dto/TokenCore.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) 2020 Tudor Stanciu - -using System.Collections.Generic; - -namespace Tuitio.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/src/Tuitio.PublishedLanguage/Events/AuthenticateUserResult.cs b/src/Tuitio.PublishedLanguage/Events/AuthenticateUserResult.cs deleted file mode 100644 index f73cb3c..0000000 --- a/src/Tuitio.PublishedLanguage/Events/AuthenticateUserResult.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2020 Tudor Stanciu - -using Tuitio.PublishedLanguage.Dto; - -namespace Tuitio.PublishedLanguage.Events -{ - public class AuthenticateUserResult - { - public Token Token { get; set; } - public string Status { get; set; } - } -} \ No newline at end of file diff --git a/src/Tuitio.PublishedLanguage/ReleaseNotes.txt b/src/Tuitio.PublishedLanguage/ReleaseNotes.txt index efdeed3..7d36bc8 100644 --- a/src/Tuitio.PublishedLanguage/ReleaseNotes.txt +++ b/src/Tuitio.PublishedLanguage/ReleaseNotes.txt @@ -1,3 +1,7 @@ -2.0.0 release [2023-01-31 02:17] +2.1.0 release [2023-03-07 22:17] +◾ Tuitio refactoring +◾ Added account logout method + +2.0.0 release [2023-01-31 02:17] ◾ Tuitio rebranding ◾ Initial release of Tuitio's published language package \ No newline at end of file diff --git a/src/Tuitio.PublishedLanguage/Tuitio.PublishedLanguage.csproj b/src/Tuitio.PublishedLanguage/Tuitio.PublishedLanguage.csproj index 90b1888..ba0a4e4 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.0.0 + 2.1.0 logo.png README.md Toodle HomeLab diff --git a/src/Tuitio.Wrapper/Constants/ApiRoutes.cs b/src/Tuitio.Wrapper/Constants/ApiRoutes.cs index aa40734..5501cb0 100644 --- a/src/Tuitio.Wrapper/Constants/ApiRoutes.cs +++ b/src/Tuitio.Wrapper/Constants/ApiRoutes.cs @@ -5,7 +5,8 @@ namespace Tuitio.Wrapper.Constants internal struct ApiRoutes { public const string - Authentication = "identity/authenticate?UserName={0}&Password={1}", - Authorization = "identity/authorize?Token={0}"; + AccountLogin = "account/login?UserName={0}&Password={1}", + AccountLogout = "account/logout?Token={0}", + Authorization = "connect/authorize?Token={0}"; } } diff --git a/src/Tuitio.Wrapper/DependencyInjectionExtension.cs b/src/Tuitio.Wrapper/DependencyInjectionExtension.cs index 49c7757..af56c0f 100644 --- a/src/Tuitio.Wrapper/DependencyInjectionExtension.cs +++ b/src/Tuitio.Wrapper/DependencyInjectionExtension.cs @@ -11,7 +11,7 @@ namespace Tuitio.Wrapper public static void UseTuitioServices(this IServiceCollection services, string baseAddress) { services.AddSingleton(new ServiceConfiguration(baseAddress)); - services.AddHttpClient(); + services.AddHttpClient(); } } } diff --git a/src/Tuitio.Wrapper/README.md b/src/Tuitio.Wrapper/README.md index e373a2c..6177855 100644 --- a/src/Tuitio.Wrapper/README.md +++ b/src/Tuitio.Wrapper/README.md @@ -2,9 +2,9 @@ [Tuitio](https://lab.code-rove.com/gitea/tudor.stanciu/tuitio) is a simple identity server implementation focused strictly on the needs of my home lab. -***Tuitio.Wrapper*** is a NuGet package that facilitates integration with a Tuitio instance in a .NET environment by registering a service called IIdentityService in the application's service collection. -It contains two methods, "Authenticate" and "Authorize", which are responsible for calling the appropriate methods from the API controller, ```/identity/authenticate``` or ```/identity/authorize```. These methods provide a simple and convenient way for developers to handle authentication and authorization when communicating with Tuitio's API. -Once the package is installed, all the developer has to do is call the ```UseTuitioServices``` method at application startup. After this step, IIdentityService can be injected into any class in the application. +***Tuitio.Wrapper*** is a NuGet package that facilitates integration with a Tuitio instance in a .NET environment by registering a service called ITuitioService in the application's service collection. +It contains three methods, "Login", "Logout" and "Authorize", which are responsible for calling the appropriate methods from the API controller, ```/account/login```, ```/account/logout``` or ```/connect/authorize```. These methods provide a simple and convenient way for developers to handle authentication and authorization when communicating with Tuitio's API. +Once the package is installed, all the developer has to do is call the ```UseTuitioServices``` method at application startup. After this step, ITuitioService can be injected into any class in the application. ## Package repository diff --git a/src/Tuitio.Wrapper/ReleaseNotes.txt b/src/Tuitio.Wrapper/ReleaseNotes.txt index c33c26d..899baa6 100644 --- a/src/Tuitio.Wrapper/ReleaseNotes.txt +++ b/src/Tuitio.Wrapper/ReleaseNotes.txt @@ -1,3 +1,7 @@ -2.0.0 release [2023-01-31 02:17] +2.1.0 release [2023-03-07 22:17] +◾ Tuitio refactoring +◾ Added account logout method + +2.0.0 release [2023-01-31 02:17] ◾ Tuitio rebranding ◾ Initial release of Tuitio's API wrapper \ No newline at end of file diff --git a/src/Tuitio.Wrapper/Services/IIdentityService.cs b/src/Tuitio.Wrapper/Services/IIdentityService.cs deleted file mode 100644 index 241dac9..0000000 --- a/src/Tuitio.Wrapper/Services/IIdentityService.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2020 Tudor Stanciu - -using Tuitio.PublishedLanguage.Dto; -using System.Threading.Tasks; - -namespace Tuitio.Wrapper.Services -{ - public interface IIdentityService - { - Task Authenticate(string userName, string password); - Task Authorize(string token); - } -} diff --git a/src/Tuitio.Wrapper/Services/ITuitioService.cs b/src/Tuitio.Wrapper/Services/ITuitioService.cs new file mode 100644 index 0000000..89fcb0f --- /dev/null +++ b/src/Tuitio.Wrapper/Services/ITuitioService.cs @@ -0,0 +1,14 @@ +// Copyright (c) 2020 Tudor Stanciu + +using System.Threading.Tasks; +using Tuitio.PublishedLanguage.Dto; + +namespace Tuitio.Wrapper.Services +{ + public interface ITuitioService + { + Task> Login(string userName, string password); + Task> Logout(string token); + Task> Authorize(string token); + } +} \ No newline at end of file diff --git a/src/Tuitio.Wrapper/Services/IdentityService.cs b/src/Tuitio.Wrapper/Services/IdentityService.cs deleted file mode 100644 index 2d0085f..0000000 --- a/src/Tuitio.Wrapper/Services/IdentityService.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2020 Tudor Stanciu - -using Tuitio.PublishedLanguage.Dto; -using Tuitio.Wrapper.Constants; -using Tuitio.Wrapper.Models; -using Netmash.Extensions.Http; -using System; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; - -namespace Tuitio.Wrapper.Services -{ - internal class IdentityService : IIdentityService - { - private const string _contentType = "application/json"; - private readonly HttpClient _httpClient; - - public IdentityService(HttpClient httpClient, ServiceConfiguration configuration) - { - httpClient.BaseAddress = new Uri(configuration.BaseAddress); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(_contentType)); - - _httpClient = httpClient; - } - - public async Task Authenticate(string userName, string password) - { - var route = string.Format(ApiRoutes.Authentication, userName, password); - var result = await _httpClient.ExecutePostRequest(route); - return result; - } - - public async Task Authorize(string token) - { - var route = string.Format(ApiRoutes.Authorization, token); - var result = await _httpClient.ExecutePostRequest(route); - return result; - } - } -} diff --git a/src/Tuitio.Wrapper/Services/TuitioService.cs b/src/Tuitio.Wrapper/Services/TuitioService.cs new file mode 100644 index 0000000..93d4248 --- /dev/null +++ b/src/Tuitio.Wrapper/Services/TuitioService.cs @@ -0,0 +1,48 @@ +// Copyright (c) 2020 Tudor Stanciu + +using Netmash.Extensions.Http; +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Tuitio.PublishedLanguage.Dto; +using Tuitio.Wrapper.Constants; +using Tuitio.Wrapper.Models; + +namespace Tuitio.Wrapper.Services +{ + internal class TuitioService : ITuitioService + { + private const string _contentType = "application/json"; + private readonly HttpClient _httpClient; + + public TuitioService(HttpClient httpClient, ServiceConfiguration configuration) + { + httpClient.BaseAddress = new Uri(configuration.BaseAddress); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(_contentType)); + + _httpClient = httpClient; + } + + public async Task> Login(string userName, string password) + { + var route = string.Format(ApiRoutes.AccountLogin, userName, password); + var result = await _httpClient.ExecutePostRequest>(route); + return result; + } + + public async Task> Logout(string token) + { + var route = string.Format(ApiRoutes.AccountLogout, token); + var result = await _httpClient.ExecutePostRequest>(route); + return result; + } + + public async Task> Authorize(string token) + { + var route = string.Format(ApiRoutes.Authorization, token); + var result = await _httpClient.ExecutePostRequest>(route); + return result; + } + } +} diff --git a/src/Tuitio.Wrapper/Tuitio.Wrapper.csproj b/src/Tuitio.Wrapper/Tuitio.Wrapper.csproj index 1b2ea4f..1c7fb82 100644 --- a/src/Tuitio.Wrapper/Tuitio.Wrapper.csproj +++ b/src/Tuitio.Wrapper/Tuitio.Wrapper.csproj @@ -2,12 +2,12 @@ net6.0 - Tuitio.Wrapper facilitates integration with a Tuitio instance in a .NET environment by registering a service called IIdentityService in the application’s service collection. It contains two methods that provide a simple and convenient way for developers to handle authentication and authorization when communicating with Tuitio’s API. + Tuitio.Wrapper facilitates integration with a Tuitio instance in a .NET environment by registering a service called ITuitioService in the application’s service collection. It contains three methods that provide a simple and convenient way for developers to handle authentication and authorization when communicating with Tuitio’s API. https://lab.code-rove.com/gitea/tudor.stanciu/tuitio https://lab.code-rove.com/gitea/tudor.stanciu/tuitio Git $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/ReleaseNotes.txt")) - 2.0.0 + 2.1.0 logo.png README.md Toodle HomeLab diff --git a/src/Tuitio/Controllers/AccountController.cs b/src/Tuitio/Controllers/AccountController.cs new file mode 100644 index 0000000..f5e4cac --- /dev/null +++ b/src/Tuitio/Controllers/AccountController.cs @@ -0,0 +1,37 @@ +// Copyright (c) 2020 Tudor Stanciu + +using MediatR; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Tuitio.Application.CommandHandlers; + +namespace Tuitio.Controllers +{ + [ApiController] + [Route("account")] + public class AccountController : ControllerBase + { + private readonly IMediator _mediator; + + public AccountController(IMediator mediator) + { + _mediator = mediator; + } + + [HttpPost("login")] + public async Task Login([FromQuery] string userName, string password) + { + var command = new AccountLoginHandler.Command(userName, password); + var result = await _mediator.Send(command); + return Ok(result); + } + + [HttpPost("logout")] + public async Task Logout([FromQuery] string token) + { + var command = new AccountLogoutHandler.Command(token); + var result = await _mediator.Send(command); + return Ok(result); + } + } +} diff --git a/src/Tuitio/Controllers/ConnectController.cs b/src/Tuitio/Controllers/ConnectController.cs new file mode 100644 index 0000000..e94b151 --- /dev/null +++ b/src/Tuitio/Controllers/ConnectController.cs @@ -0,0 +1,29 @@ +// Copyright (c) 2020 Tudor Stanciu + +using MediatR; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Tuitio.Application.CommandHandlers; + +namespace Tuitio.Api.Controllers +{ + [ApiController] + [Route("connect")] + public class ConnectController : ControllerBase + { + private readonly IMediator _mediator; + + public ConnectController(IMediator mediator) + { + _mediator = mediator; + } + + [HttpPost("authorize")] + public async Task AuthorizeToken([FromQuery] string token) + { + var command = new AuthorizationHandler.Command(token); + var result = await _mediator.Send(command); + return Ok(result); + } + } +} diff --git a/src/Tuitio/Controllers/IdentityController.cs b/src/Tuitio/Controllers/IdentityController.cs deleted file mode 100644 index b3f4d2d..0000000 --- a/src/Tuitio/Controllers/IdentityController.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) 2020 Tudor Stanciu - -using Tuitio.Application.Commands; -using MediatR; -using Microsoft.AspNetCore.Mvc; -using System.Threading.Tasks; - -namespace Tuitio.Api.Controllers -{ - [ApiController] - [Route("identity")] - public class IdentityController : ControllerBase - { - private readonly IMediator _mediator; - - public IdentityController(IMediator mediator) - { - _mediator = mediator; - } - - [HttpPost("authenticate")] - public async Task AuthenticateUser([FromQuery] AuthenticateUser authenticateUser) - { - var result = await _mediator.Send(authenticateUser); - - if (result != null) - return Ok(result); - else - return BadRequest(); - } - - [HttpPost("authorize")] - public async Task AuthorizeToken([FromQuery] AuthorizeToken authorizeToken) - { - var result = await _mediator.Send(authorizeToken); - - if (result != null) - return Ok(result); - else - return BadRequest(); - } - } -} diff --git a/src/Tuitio/Controllers/SystemController.cs b/src/Tuitio/Controllers/SystemController.cs index 0016823..a90e5eb 100644 --- a/src/Tuitio/Controllers/SystemController.cs +++ b/src/Tuitio/Controllers/SystemController.cs @@ -11,11 +11,8 @@ namespace Tuitio.Api.Controllers [Route("system")] public class SystemController : ControllerBase { - private readonly IMediator _mediator; - public SystemController(IMediator mediator) { - _mediator = mediator; } [AllowAnonymous] @@ -24,12 +21,5 @@ namespace Tuitio.Api.Controllers { return Ok($"Ping success. System datetime: {DateTime.Now}"); } - - /* - Methods: - /version - /burn-token - /burn-all-tokens - */ } } diff --git a/src/Tuitio/Extensions/StartupExtensions.cs b/src/Tuitio/Extensions/StartupExtensions.cs index 439d339..9d14d30 100644 --- a/src/Tuitio/Extensions/StartupExtensions.cs +++ b/src/Tuitio/Extensions/StartupExtensions.cs @@ -22,7 +22,7 @@ namespace Tuitio.Extensions services.AddControllers(); // MediatR - services.AddMediatR(typeof(Application.Commands.AuthenticateUser).Assembly); + services.AddMediatR(typeof(Application.CommandHandlers.AccountLoginHandler).Assembly); services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPreProcessorBehavior<,>)); services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPostProcessorBehavior<,>));