Merged PR 74: Tuitio refactoring and account logout implementation
Tuitio refactoring and account logout implementationmaster
commit
13ca541478
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
</Content>
|
||||
</Note>
|
||||
<Note>
|
||||
<Version>1.0.1</Version>
|
||||
<Content>
|
||||
◾ 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
|
||||
</Content>
|
||||
</Note>
|
||||
<Note>
|
||||
<Version>2.1.0</Version>
|
||||
<Content>
|
||||
◾ Tuitio refactoring
|
||||
◾ Added account logout method
|
||||
◾ Tuitio performance optimizations
|
||||
</Content>
|
||||
</Note>
|
||||
</ReleaseNotes>
|
|
@ -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<Envelope<AccountLoginResult>>;
|
||||
|
||||
public class CommandHandler : IRequestHandler<Command, Envelope<AccountLoginResult>>
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly ILogger<AccountLoginHandler> _logger;
|
||||
|
||||
public CommandHandler(IUserService userService, ILogger<AccountLoginHandler> logger)
|
||||
{
|
||||
_userService = userService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Envelope<AccountLoginResult>> 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<AccountLoginResult>.Fail(EnvelopeError.BAD_CREDENTIALS);
|
||||
}
|
||||
|
||||
_logger.LogDebug($"Login succeeded for user '{command.UserName}'.");
|
||||
|
||||
var result = new AccountLoginResult(loginResult.Raw, loginResult.Token.ExpiresIn);
|
||||
return Envelope<AccountLoginResult>.Success(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Envelope<AccountLogoutResult>>;
|
||||
|
||||
public class CommandHandler : IRequestHandler<Command, Envelope<AccountLogoutResult>>
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly ILogger<AccountLoginHandler> _logger;
|
||||
|
||||
public CommandHandler(IUserService userService, IMapper mapper, ILogger<AccountLoginHandler> logger)
|
||||
{
|
||||
_userService = userService;
|
||||
_mapper = mapper;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Envelope<AccountLogoutResult>> 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<AccountLogoutResult>.Fail(EnvelopeError.UNAUTHENTICATED);
|
||||
}
|
||||
|
||||
_logger.LogDebug($"Logout succeeded for user '{logoutResult.UserName}'.");
|
||||
|
||||
var result = _mapper.Map<AccountLogoutResult>(logoutResult);
|
||||
return Envelope<AccountLogoutResult>.Success(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<AuthenticateUser, AuthenticateUserResult>
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly ILogger<AuthenticateUserHandler> _logger;
|
||||
|
||||
public AuthenticateUserHandler(IUserService userService, IMapper mapper, ILogger<AuthenticateUserHandler> logger)
|
||||
{
|
||||
_userService = userService;
|
||||
_mapper = mapper;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<AuthenticateUserResult> 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<Token>(internalToken);
|
||||
|
||||
return new AuthenticateUserResult() { Token = token, Status = AuthenticationStatus.SUCCESS };
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Envelope<AuthorizationResult>>;
|
||||
|
||||
public class CommandHandler : IRequestHandler<Command, Envelope<AuthorizationResult>>
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly ILogger<AuthorizationHandler> _logger;
|
||||
|
||||
public CommandHandler(IUserService userService, IMapper mapper, ILogger<AuthorizationHandler> logger)
|
||||
{
|
||||
_userService = userService;
|
||||
_mapper = mapper;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<Envelope<AuthorizationResult>> 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<AuthorizationResult>.Fail(EnvelopeError.UNAUTHORIZED);
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
_logger.LogDebug($"Authorization succeeded for token '{command.Token}'.");
|
||||
var authorizationResult = _mapper.Map<AuthorizationResult>(token);
|
||||
var envelope = Envelope<AuthorizationResult>.Success(authorizationResult);
|
||||
return Task.FromResult(envelope);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<AuthorizeToken, TokenCore>
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly ILogger<AuthorizeTokenHandler> _logger;
|
||||
|
||||
public AuthorizeTokenHandler(IUserService userService, IMapper mapper, ILogger<AuthorizeTokenHandler> logger)
|
||||
{
|
||||
_userService = userService;
|
||||
_mapper = mapper;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<TokenCore> 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>(tokenCore);
|
||||
|
||||
return Task.FromResult(tokenCoreResult);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
// Copyright (c) 2020 Tudor Stanciu
|
||||
|
||||
using MediatR;
|
||||
using Tuitio.PublishedLanguage.Events;
|
||||
|
||||
namespace Tuitio.Application.Commands
|
||||
{
|
||||
public class AuthenticateUser : IRequest<AuthenticateUserResult>
|
||||
{
|
||||
public string UserName { get; set; }
|
||||
public string Password { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
// Copyright (c) 2020 Tudor Stanciu
|
||||
|
||||
using MediatR;
|
||||
using Tuitio.PublishedLanguage.Dto;
|
||||
|
||||
namespace Tuitio.Application.Commands
|
||||
{
|
||||
public class AuthorizeToken : IRequest<TokenCore>
|
||||
{
|
||||
public string Token { get; set; }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -12,10 +12,11 @@ namespace Tuitio.Application.Mappings
|
|||
{
|
||||
public MappingProfile()
|
||||
{
|
||||
CreateMap<models.Token, dto.Token>();
|
||||
CreateMap<models.TokenCore, dto.TokenCore>();
|
||||
CreateMap<AppUser, models.TokenCore>()
|
||||
CreateMap<models.Token, dto.AuthorizationResult>();
|
||||
CreateMap<AppUser, models.Token>()
|
||||
.ForMember(z => z.Claims, src => src.MapFrom(z => ComposeClaims(z.Claims)));
|
||||
|
||||
CreateMap<models.Account.LogoutResult, dto.AccountLogoutResult>();
|
||||
}
|
||||
|
||||
private Dictionary<string, string> ComposeClaims(ICollection<UserClaim> claims)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<LoginResult> Login(string userName, string password);
|
||||
Task<LogoutResult> Logout(string tokenRaw);
|
||||
Token Authorize(string tokenRaw);
|
||||
}
|
||||
}
|
|
@ -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<BehaviorService> _logger;
|
||||
private readonly ITokenService _tokenService;
|
||||
private readonly ITokenStore _securityStore;
|
||||
|
||||
public BehaviorService(IServiceProvider serviceProvider, ILogger<BehaviorService> logger, ITokenStore securityStore)
|
||||
public BehaviorService(IServiceProvider serviceProvider, ILogger<BehaviorService> 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<UserToken>();
|
||||
using (var scope = _serviceProvider.CreateScope())
|
||||
{
|
||||
var _repository = scope.ServiceProvider.GetRequiredService<IIdentityRepository>();
|
||||
var _repository = scope.ServiceProvider.GetRequiredService<IUserRepository>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Token> Authenticate(string userName, string password);
|
||||
TokenCore Authorize(string token);
|
||||
}
|
||||
}
|
|
@ -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<Token>(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<TokenCore>(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<TokenCore>(tokenCoreString);
|
||||
return tokenCore;
|
||||
var tokenBytes = Convert.FromBase64String(tokenRaw);
|
||||
var tokenString = Encoding.UTF8.GetString(tokenBytes);
|
||||
var token = JsonConvert.DeserializeObject<Token>(tokenString);
|
||||
return token;
|
||||
}
|
||||
|
||||
private bool ValidateTokenRaw(string tokenRaw)
|
||||
|
|
|
@ -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<Token> Authenticate(string userName, string password)
|
||||
public async Task<LoginResult> 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<LogoutResult> 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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<int, List<Token>> Tokens { get; }
|
||||
private ConcurrentDictionary<string, Token> Tokens { get; }
|
||||
|
||||
public TokenStore(ITokenService tokenService)
|
||||
public TokenStore()
|
||||
{
|
||||
_tokenService = tokenService;
|
||||
Tokens = new ConcurrentDictionary<int, List<Token>>();
|
||||
Tokens = new ConcurrentDictionary<string, Token>();
|
||||
}
|
||||
|
||||
public void SetToken(Token token, int userId)
|
||||
public Token Get(string key)
|
||||
{
|
||||
var registered = Tokens.TryGetValue(userId, out List<Token> list);
|
||||
|
||||
if (registered)
|
||||
list.Add(token);
|
||||
else
|
||||
Tokens.TryAdd(userId, new List<Token>() { token });
|
||||
}
|
||||
|
||||
public TokenCore ValidateAndGetTokenCore(string token)
|
||||
{
|
||||
var tokenCore = _tokenService.ExtractTokenCore(token);
|
||||
if (tokenCore == null)
|
||||
return null;
|
||||
|
||||
var registered = Tokens.TryGetValue(tokenCore.UserId, out List<Token> 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 _);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,12 +6,12 @@ using Microsoft.EntityFrameworkCore;
|
|||
|
||||
namespace Tuitio.Domain.Data.DbContexts
|
||||
{
|
||||
public class IdentityDbContext : DbContext
|
||||
public class TuitioDbContext : DbContext
|
||||
{
|
||||
public DbSet<AppUser> Users { get; set; }
|
||||
public DbSet<UserToken> UserTokens { get; set; }
|
||||
|
||||
public IdentityDbContext(DbContextOptions<IdentityDbContext> options)
|
||||
public TuitioDbContext(DbContextOptions<TuitioDbContext> options)
|
||||
: base(options)
|
||||
{
|
||||
base.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll;
|
|
@ -13,10 +13,10 @@ namespace Tuitio.Domain.Data
|
|||
{
|
||||
public static void AddDataAccess(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IIdentityRepository, IdentityRepository>();
|
||||
services.AddScoped<IUserRepository, UserRepository>();
|
||||
|
||||
services
|
||||
.AddDbContextPool<IdentityDbContext>(
|
||||
.AddDbContextPool<TuitioDbContext>(
|
||||
(serviceProvider, options) =>
|
||||
{
|
||||
var configuration = serviceProvider.GetService<IConfiguration>();
|
||||
|
|
|
@ -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<UserToken> builder)
|
||||
{
|
||||
builder.ToTable("UserToken").HasKey(z => z.TokenId);
|
||||
builder.Property(z => z.TokenId).ValueGeneratedOnAdd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<AppUser> 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<UserToken[]> 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<AppUser> 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<UserToken[]> 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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<string, string> Claims { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<string, string> Claims { get; set; }
|
||||
}
|
||||
}
|
|
@ -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<AppUser> GetUser(string userName, string password);
|
||||
Task UpdateUserAfterAuthentication(AppUser user, Token token);
|
||||
Task UpdateUserAfterLogin(AppUser user, Token token, string tokenRaw);
|
||||
Task<UserToken[]> GetActiveTokens();
|
||||
Task RemoveToken(Guid tokenId);
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright (c) 2020 Tudor Stanciu
|
||||
|
||||
namespace Tuitio.PublishedLanguage.Dto
|
||||
{
|
||||
public class Envelope<T> where T : class
|
||||
{
|
||||
public T Result { get; }
|
||||
public string Error { get; }
|
||||
|
||||
public Envelope(T result, string error)
|
||||
{
|
||||
Result=result;
|
||||
Error=error;
|
||||
}
|
||||
|
||||
public static Envelope<T> Success(T result)
|
||||
{
|
||||
return new Envelope<T>(result, null);
|
||||
}
|
||||
|
||||
public static Envelope<T> Fail(string message)
|
||||
{
|
||||
return new Envelope<T>(null, message);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<string, string> Claims { get; init; }
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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<string, string> Claims { get; set; }
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -7,7 +7,7 @@
|
|||
<RepositoryUrl>https://lab.code-rove.com/gitea/tudor.stanciu/tuitio</RepositoryUrl>
|
||||
<RepositoryType>Git</RepositoryType>
|
||||
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/ReleaseNotes.txt"))</PackageReleaseNotes>
|
||||
<Version>2.0.0</Version>
|
||||
<Version>2.1.0</Version>
|
||||
<PackageIcon>logo.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<Company>Toodle HomeLab</Company>
|
||||
|
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ namespace Tuitio.Wrapper
|
|||
public static void UseTuitioServices(this IServiceCollection services, string baseAddress)
|
||||
{
|
||||
services.AddSingleton(new ServiceConfiguration(baseAddress));
|
||||
services.AddHttpClient<IIdentityService, IdentityService>();
|
||||
services.AddHttpClient<ITuitioService, TuitioService>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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<Token> Authenticate(string userName, string password);
|
||||
Task<TokenCore> Authorize(string token);
|
||||
}
|
||||
}
|
|
@ -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<Envelope<AccountLoginResult>> Login(string userName, string password);
|
||||
Task<Envelope<AccountLogoutResult>> Logout(string token);
|
||||
Task<Envelope<AuthorizationResult>> Authorize(string token);
|
||||
}
|
||||
}
|
|
@ -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<Token> Authenticate(string userName, string password)
|
||||
{
|
||||
var route = string.Format(ApiRoutes.Authentication, userName, password);
|
||||
var result = await _httpClient.ExecutePostRequest<Token>(route);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<TokenCore> Authorize(string token)
|
||||
{
|
||||
var route = string.Format(ApiRoutes.Authorization, token);
|
||||
var result = await _httpClient.ExecutePostRequest<TokenCore>(route);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Envelope<AccountLoginResult>> Login(string userName, string password)
|
||||
{
|
||||
var route = string.Format(ApiRoutes.AccountLogin, userName, password);
|
||||
var result = await _httpClient.ExecutePostRequest<Envelope<AccountLoginResult>>(route);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<Envelope<AccountLogoutResult>> Logout(string token)
|
||||
{
|
||||
var route = string.Format(ApiRoutes.AccountLogout, token);
|
||||
var result = await _httpClient.ExecutePostRequest<Envelope<AccountLogoutResult>>(route);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<Envelope<AuthorizationResult>> Authorize(string token)
|
||||
{
|
||||
var route = string.Format(ApiRoutes.Authorization, token);
|
||||
var result = await _httpClient.ExecutePostRequest<Envelope<AuthorizationResult>>(route);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Description>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.</Description>
|
||||
<Description>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.</Description>
|
||||
<PackageProjectUrl>https://lab.code-rove.com/gitea/tudor.stanciu/tuitio</PackageProjectUrl>
|
||||
<RepositoryUrl>https://lab.code-rove.com/gitea/tudor.stanciu/tuitio</RepositoryUrl>
|
||||
<RepositoryType>Git</RepositoryType>
|
||||
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/ReleaseNotes.txt"))</PackageReleaseNotes>
|
||||
<Version>2.0.0</Version>
|
||||
<Version>2.1.0</Version>
|
||||
<PackageIcon>logo.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<Company>Toodle HomeLab</Company>
|
||||
|
|
|
@ -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<IActionResult> 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<IActionResult> Logout([FromQuery] string token)
|
||||
{
|
||||
var command = new AccountLogoutHandler.Command(token);
|
||||
var result = await _mediator.Send(command);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<IActionResult> AuthorizeToken([FromQuery] string token)
|
||||
{
|
||||
var command = new AuthorizationHandler.Command(token);
|
||||
var result = await _mediator.Send(command);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<IActionResult> AuthenticateUser([FromQuery] AuthenticateUser authenticateUser)
|
||||
{
|
||||
var result = await _mediator.Send(authenticateUser);
|
||||
|
||||
if (result != null)
|
||||
return Ok(result);
|
||||
else
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
[HttpPost("authorize")]
|
||||
public async Task<IActionResult> AuthorizeToken([FromQuery] AuthorizeToken authorizeToken)
|
||||
{
|
||||
var result = await _mediator.Send(authorizeToken);
|
||||
|
||||
if (result != null)
|
||||
return Ok(result);
|
||||
else
|
||||
return BadRequest();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<,>));
|
||||
|
||||
|
|
Loading…
Reference in New Issue