Merged PR 74: Tuitio refactoring and account logout implementation

Tuitio refactoring and account logout implementation
master
Tudor Stanciu 2023-03-08 16:42:14 +00:00
commit 13ca541478
53 changed files with 595 additions and 485 deletions

View File

@ -1,9 +1,10 @@
# Tuitio # Tuitio
Tuitio is a simple identity server implementation focused strictly on the needs of my home lab. 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: At the moment it has a simple API consisting of only three methods:
* ```/identity/authenticate``` - handles user authentication using credentials and generates an access token. * ```/account/login``` - 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. * ```/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. ***Tuitio*** is a latin word that encompasses meanings such as supervision, safeguarding, defense, guard duty, and protection.

View File

@ -8,16 +8,16 @@
A client/consumer can do only two things: 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" } - 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: "***" } - 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.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 - The source of this nugets is public, but on my personal server: https://lab.code-rove.com/public-nuget-server/nuget
</Content> </Content>
</Note> </Note>
<Note> <Note>
<Version>1.0.1</Version> <Version>1.0.1</Version>
<Content> <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 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. ◾ 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. ◾ 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 ◾ Added README.md file
</Content> </Content>
</Note> </Note>
<Note>
<Version>2.1.0</Version>
<Content>
◾ Tuitio refactoring
◾ Added account logout method
◾ Tuitio performance optimizations
</Content>
</Note>
</ReleaseNotes> </ReleaseNotes>

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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 };
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -1,10 +1,10 @@
// Copyright (c) 2020 Tudor Stanciu // Copyright (c) 2020 Tudor Stanciu
using Microsoft.Extensions.DependencyInjection;
using Tuitio.Application.Services; using Tuitio.Application.Services;
using Tuitio.Application.Services.Abstractions; using Tuitio.Application.Services.Abstractions;
using Tuitio.Application.Stores; using Tuitio.Application.Stores;
using Tuitio.Domain.Abstractions; using Tuitio.Domain.Abstractions;
using Microsoft.Extensions.DependencyInjection;
namespace Tuitio.Application namespace Tuitio.Application
{ {

View File

@ -12,10 +12,11 @@ namespace Tuitio.Application.Mappings
{ {
public MappingProfile() public MappingProfile()
{ {
CreateMap<models.Token, dto.Token>(); CreateMap<models.Token, dto.AuthorizationResult>();
CreateMap<models.TokenCore, dto.TokenCore>(); CreateMap<AppUser, models.Token>()
CreateMap<AppUser, models.TokenCore>()
.ForMember(z => z.Claims, src => src.MapFrom(z => ComposeClaims(z.Claims))); .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) private Dictionary<string, string> ComposeClaims(ICollection<UserClaim> claims)

View File

@ -3,11 +3,12 @@
using Tuitio.Domain.Entities; using Tuitio.Domain.Entities;
using Tuitio.Domain.Models; using Tuitio.Domain.Models;
namespace Tuitio.Application.Services namespace Tuitio.Application.Services.Abstractions
{ {
internal interface ITokenService internal interface ITokenService
{ {
Token GenerateToken(AppUser user); Token GenerateToken(AppUser user);
TokenCore ExtractTokenCore(string tokenRaw); string GenerateTokenRaw(Token token);
Token ExtractToken(string tokenRaw);
} }
} }

View File

@ -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);
}
}

View File

@ -1,14 +1,13 @@
// Copyright (c) 2020 Tudor Stanciu // 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.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.Threading.Tasks; 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 namespace Tuitio.Application.Services
{ {
@ -16,13 +15,15 @@ namespace Tuitio.Application.Services
{ {
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly ILogger<BehaviorService> _logger; private readonly ILogger<BehaviorService> _logger;
private readonly ITokenService _tokenService;
private readonly ITokenStore _securityStore; 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; _serviceProvider=serviceProvider;
_logger = logger; _logger=logger;
_securityStore = securityStore; _tokenService=tokenService;
_securityStore=securityStore;
} }
public void FillTokenStore() public void FillTokenStore()
@ -33,7 +34,7 @@ namespace Tuitio.Application.Services
var activeTokens = Array.Empty<UserToken>(); var activeTokens = Array.Empty<UserToken>();
using (var scope = _serviceProvider.CreateScope()) using (var scope = _serviceProvider.CreateScope())
{ {
var _repository = scope.ServiceProvider.GetRequiredService<IIdentityRepository>(); var _repository = scope.ServiceProvider.GetRequiredService<IUserRepository>();
activeTokens = await _repository.GetActiveTokens(); activeTokens = await _repository.GetActiveTokens();
} }
@ -43,8 +44,8 @@ namespace Tuitio.Application.Services
_logger.LogInformation($"BehaviorService: {activeTokens.Length} active tokens were found in database."); _logger.LogInformation($"BehaviorService: {activeTokens.Length} active tokens were found in database.");
foreach (var token in activeTokens) foreach (var token in activeTokens)
{ {
var storeToken = new Token() { Raw = token.Token, ValidFrom = token.ValidFrom, ValidUntil = token.ValidUntil }; var storeToken = _tokenService.ExtractToken(token.Token);
_securityStore.SetToken(storeToken, token.UserId); _securityStore.Set(token.Token, storeToken);
} }
} }
} }

View File

@ -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);
}
}

View File

@ -1,13 +1,15 @@
// Copyright (c) 2020 Tudor Stanciu // Copyright (c) 2020 Tudor Stanciu
using AutoMapper; using AutoMapper;
using Tuitio.Domain.Abstractions;
using Tuitio.Domain.Entities;
using Tuitio.Domain.Models;
using Newtonsoft.Json; using Newtonsoft.Json;
using System; using System;
using System.Text; using System.Text;
using System.Text.RegularExpressions; 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 namespace Tuitio.Application.Services
{ {
@ -15,47 +17,46 @@ namespace Tuitio.Application.Services
{ {
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly IConfigProvider _configProvider; private readonly IConfigProvider _configProvider;
private readonly ITokenStore _tokenStore;
public TokenService(IMapper mapper, IConfigProvider configProvider) public TokenService(IMapper mapper, IConfigProvider configProvider, ITokenStore tokenStore)
{ {
_mapper = mapper; _mapper=mapper;
_configProvider = configProvider; _configProvider=configProvider;
_tokenStore=tokenStore;
} }
public Token GenerateToken(AppUser user) public Token GenerateToken(AppUser user)
{ {
var tokenRaw = GenerateTokenRaw(user); var currentDate = DateTime.UtcNow;
var currentDate = DateTime.Now; var validUntil = currentDate.AddMinutes(_configProvider.Token.ValidityInMinutes);
var token = new Token() { Raw = tokenRaw, ValidFrom = currentDate, 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; return token;
} }
private string GenerateTokenRaw(AppUser user) public string GenerateTokenRaw(Token token)
{ {
var tokenCore = GenerateTokenCore(user); var tokenString = JsonConvert.SerializeObject(token);
var tokenCoreString = JsonConvert.SerializeObject(tokenCore); var tokenBytes = Encoding.UTF8.GetBytes(tokenString);
var tokenCoreBytes = Encoding.UTF8.GetBytes(tokenCoreString); var tokenRaw = Convert.ToBase64String(tokenBytes);
var tokenRaw = Convert.ToBase64String(tokenCoreBytes);
return tokenRaw; return tokenRaw;
} }
private TokenCore GenerateTokenCore(AppUser user) public Token ExtractToken(string tokenRaw)
{
var tokenCore = _mapper.Map<TokenCore>(user);
tokenCore.LockStamp = Regex.Replace(Convert.ToBase64String(Guid.NewGuid().ToByteArray()), "[/+=]", "");
return tokenCore;
}
public TokenCore ExtractTokenCore(string tokenRaw)
{ {
var valid = ValidateTokenRaw(tokenRaw); var valid = ValidateTokenRaw(tokenRaw);
if (!valid) if (!valid)
return null; return null;
var tokenCoreBytes = Convert.FromBase64String(tokenRaw); var tokenBytes = Convert.FromBase64String(tokenRaw);
var tokenCoreString = Encoding.UTF8.GetString(tokenCoreBytes); var tokenString = Encoding.UTF8.GetString(tokenBytes);
var tokenCore = JsonConvert.DeserializeObject<TokenCore>(tokenCoreString); var token = JsonConvert.DeserializeObject<Token>(tokenString);
return tokenCore; return token;
} }
private bool ValidateTokenRaw(string tokenRaw) private bool ValidateTokenRaw(string tokenRaw)

View File

@ -1,61 +1,78 @@
// Copyright (c) 2020 Tudor Stanciu // Copyright (c) 2020 Tudor Stanciu
using System;
using System.Threading.Tasks;
using Tuitio.Application.Services.Abstractions; using Tuitio.Application.Services.Abstractions;
using Tuitio.Application.Stores; using Tuitio.Application.Stores;
using Tuitio.Domain.Abstractions; using Tuitio.Domain.Abstractions;
using Tuitio.Domain.Entities; using Tuitio.Domain.Entities;
using Tuitio.Domain.Models; using Tuitio.Domain.Models;
using Tuitio.Domain.Models.Account;
using Tuitio.Domain.Repositories; using Tuitio.Domain.Repositories;
using System;
using System.Threading.Tasks;
namespace Tuitio.Application.Services namespace Tuitio.Application.Services
{ {
internal class UserService : IUserService internal class UserService : IUserService
{ {
private readonly ITokenStore _securityStore; private readonly ITokenStore _securityStore;
private readonly IIdentityRepository _identityRepository; private readonly IUserRepository _userRepository;
private readonly ITokenService _tokenService; private readonly ITokenService _tokenService;
private readonly IConfigProvider _configProvider; private readonly IConfigProvider _configProvider;
private readonly IHashingService _hashingService; 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; _securityStore=securityStore;
_identityRepository=identityRepository; _userRepository=userRepository;
_tokenService=tokenService; _tokenService=tokenService;
_configProvider=configProvider; _configProvider=configProvider;
_hashingService=hashingService; _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 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); var valid = ValidateUser(user);
if (!valid) if (!valid)
return null; return null;
var token = _tokenService.GenerateToken(user); var token = _tokenService.GenerateToken(user);
_securityStore.SetToken(token, user.UserId); var raw = _tokenService.GenerateTokenRaw(token);
await _identityRepository.UpdateUserAfterAuthentication(user, 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); var token = _securityStore.Get(tokenRaw);
if (tokenCore == null) if (token == null)
return 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) private bool ValidateUser(AppUser user)
{ {
if (user == null) if (user == null)
return false; throw new ArgumentNullException(nameof(user));
if (user.FailedLoginAttempts.HasValue && user.FailedLoginAttempts.Value > _configProvider.Restrictions.MaxFailedLoginAttempts) if (user.FailedLoginAttempts.HasValue && user.FailedLoginAttempts.Value > _configProvider.Restrictions.MaxFailedLoginAttempts)
return false; return false;

View File

@ -1,12 +1,14 @@
// Copyright (c) 2020 Tudor Stanciu // Copyright (c) 2020 Tudor Stanciu
using System;
using Tuitio.Domain.Models; using Tuitio.Domain.Models;
namespace Tuitio.Application.Stores namespace Tuitio.Application.Stores
{ {
internal interface ITokenStore internal interface ITokenStore
{ {
void SetToken(Token token, int userId); Token Get(string key);
TokenCore ValidateAndGetTokenCore(string token); bool Set(string key, Token token);
void Remove(string key);
} }
} }

View File

@ -1,49 +1,39 @@
// Copyright (c) 2020 Tudor Stanciu // Copyright (c) 2020 Tudor Stanciu
using Tuitio.Application.Services;
using Tuitio.Domain.Models;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using Tuitio.Domain.Models;
namespace Tuitio.Application.Stores namespace Tuitio.Application.Stores
{ {
internal class TokenStore : ITokenStore internal class TokenStore : ITokenStore
{ {
private readonly ITokenService _tokenService; private ConcurrentDictionary<string, Token> Tokens { get; }
private ConcurrentDictionary<int, List<Token>> Tokens { get; }
public TokenStore(ITokenService tokenService) public TokenStore()
{ {
_tokenService = tokenService; Tokens = new ConcurrentDictionary<string, Token>();
Tokens = new ConcurrentDictionary<int, List<Token>>();
} }
public void SetToken(Token token, int userId) public Token Get(string key)
{ {
var registered = Tokens.TryGetValue(userId, out List<Token> list); var registered = Tokens.ContainsKey(key);
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);
if (!registered) if (!registered)
return null; return null;
var valid = list.FirstOrDefault(z => z.Raw == token); return Tokens[key];
if (valid == null)
return null;
return tokenCore;
} }
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 _);
} }
} }

View File

@ -6,12 +6,12 @@ using Microsoft.EntityFrameworkCore;
namespace Tuitio.Domain.Data.DbContexts namespace Tuitio.Domain.Data.DbContexts
{ {
public class IdentityDbContext : DbContext public class TuitioDbContext : DbContext
{ {
public DbSet<AppUser> Users { get; set; } public DbSet<AppUser> Users { get; set; }
public DbSet<UserToken> UserTokens { get; set; } public DbSet<UserToken> UserTokens { get; set; }
public IdentityDbContext(DbContextOptions<IdentityDbContext> options) public TuitioDbContext(DbContextOptions<TuitioDbContext> options)
: base(options) : base(options)
{ {
base.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll; base.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll;

View File

@ -13,10 +13,10 @@ namespace Tuitio.Domain.Data
{ {
public static void AddDataAccess(this IServiceCollection services) public static void AddDataAccess(this IServiceCollection services)
{ {
services.AddScoped<IIdentityRepository, IdentityRepository>(); services.AddScoped<IUserRepository, UserRepository>();
services services
.AddDbContextPool<IdentityDbContext>( .AddDbContextPool<TuitioDbContext>(
(serviceProvider, options) => (serviceProvider, options) =>
{ {
var configuration = serviceProvider.GetService<IConfiguration>(); var configuration = serviceProvider.GetService<IConfiguration>();

View File

@ -1,8 +1,8 @@
// Copyright (c) 2020 Tudor Stanciu // Copyright (c) 2020 Tudor Stanciu
using Tuitio.Domain.Entities;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Tuitio.Domain.Entities;
namespace Tuitio.Domain.Data.EntityTypeConfiguration namespace Tuitio.Domain.Data.EntityTypeConfiguration
{ {
@ -11,7 +11,6 @@ namespace Tuitio.Domain.Data.EntityTypeConfiguration
public void Configure(EntityTypeBuilder<UserToken> builder) public void Configure(EntityTypeBuilder<UserToken> builder)
{ {
builder.ToTable("UserToken").HasKey(z => z.TokenId); builder.ToTable("UserToken").HasKey(z => z.TokenId);
builder.Property(z => z.TokenId).ValueGeneratedOnAdd();
} }
} }
} }

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -2,11 +2,10 @@ if not exists (select top 1 1 from sys.objects where name = 'UserToken' and type
begin begin
create table UserToken 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), UserId int not null constraint FK_Token_AppUser foreign key references AppUser(UserId),
Token varchar(1000) not null, Token varchar(1000) not null,
ValidFrom datetime not null, ValidFrom datetime not null,
ValidUntil datetime not null, ValidUntil datetime not null
Burnt bit
) )
end end

View File

@ -6,11 +6,10 @@ namespace Tuitio.Domain.Entities
{ {
public class UserToken public class UserToken
{ {
public int TokenId { get; set; } public Guid TokenId { get; set; }
public int UserId { get; set; } public int UserId { get; set; }
public string Token { get; set; } public string Token { get; set; }
public DateTime ValidFrom { get; set; } public DateTime ValidFrom { get; set; }
public DateTime ValidUntil { get; set; } public DateTime ValidUntil { get; set; }
public bool? Burnt { get; set; }
} }
} }

View File

@ -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);
}

View File

@ -1,13 +1,23 @@
// Copyright (c) 2020 Tudor Stanciu // Copyright (c) 2020 Tudor Stanciu
using System; using System;
using System.Collections.Generic;
namespace Tuitio.Domain.Models namespace Tuitio.Domain.Models
{ {
public class Token public class Token
{ {
public string Raw { get; set; } public Guid TokenId { get; set; }
public DateTime ValidFrom { get; set; } public int UserId { get; set; }
public DateTime ValidUntil { 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; }
} }
} }

View File

@ -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; }
}
}

View File

@ -3,13 +3,15 @@
using Tuitio.Domain.Entities; using Tuitio.Domain.Entities;
using Tuitio.Domain.Models; using Tuitio.Domain.Models;
using System.Threading.Tasks; using System.Threading.Tasks;
using System;
namespace Tuitio.Domain.Repositories namespace Tuitio.Domain.Repositories
{ {
public interface IIdentityRepository public interface IUserRepository
{ {
Task<AppUser> GetUser(string userName, string password); 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<UserToken[]> GetActiveTokens();
Task RemoveToken(Guid tokenId);
} }
} }

View File

@ -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";
}
}

View File

@ -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";
}
}

View File

@ -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);
}
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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 ◾ Tuitio rebranding
◾ Initial release of Tuitio's published language package ◾ Initial release of Tuitio's published language package

View File

@ -7,7 +7,7 @@
<RepositoryUrl>https://lab.code-rove.com/gitea/tudor.stanciu/tuitio</RepositoryUrl> <RepositoryUrl>https://lab.code-rove.com/gitea/tudor.stanciu/tuitio</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/ReleaseNotes.txt"))</PackageReleaseNotes> <PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/ReleaseNotes.txt"))</PackageReleaseNotes>
<Version>2.0.0</Version> <Version>2.1.0</Version>
<PackageIcon>logo.png</PackageIcon> <PackageIcon>logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<Company>Toodle HomeLab</Company> <Company>Toodle HomeLab</Company>

View File

@ -5,7 +5,8 @@ namespace Tuitio.Wrapper.Constants
internal struct ApiRoutes internal struct ApiRoutes
{ {
public const string public const string
Authentication = "identity/authenticate?UserName={0}&Password={1}", AccountLogin = "account/login?UserName={0}&Password={1}",
Authorization = "identity/authorize?Token={0}"; AccountLogout = "account/logout?Token={0}",
Authorization = "connect/authorize?Token={0}";
} }
} }

View File

@ -11,7 +11,7 @@ namespace Tuitio.Wrapper
public static void UseTuitioServices(this IServiceCollection services, string baseAddress) public static void UseTuitioServices(this IServiceCollection services, string baseAddress)
{ {
services.AddSingleton(new ServiceConfiguration(baseAddress)); services.AddSingleton(new ServiceConfiguration(baseAddress));
services.AddHttpClient<IIdentityService, IdentityService>(); services.AddHttpClient<ITuitioService, TuitioService>();
} }
} }
} }

View File

@ -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](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. ***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 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. 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, IIdentityService can be injected into any class in the application. 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 ## Package repository

View File

@ -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 ◾ Tuitio rebranding
◾ Initial release of Tuitio's API wrapper ◾ Initial release of Tuitio's API wrapper

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -2,12 +2,12 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <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 applications service collection. It contains two methods that provide a simple and convenient way for developers to handle authentication and authorization when communicating with Tuitios API.</Description> <Description>Tuitio.Wrapper facilitates integration with a Tuitio instance in a .NET environment by registering a service called ITuitioService in the applications service collection. It contains three methods that provide a simple and convenient way for developers to handle authentication and authorization when communicating with Tuitios API.</Description>
<PackageProjectUrl>https://lab.code-rove.com/gitea/tudor.stanciu/tuitio</PackageProjectUrl> <PackageProjectUrl>https://lab.code-rove.com/gitea/tudor.stanciu/tuitio</PackageProjectUrl>
<RepositoryUrl>https://lab.code-rove.com/gitea/tudor.stanciu/tuitio</RepositoryUrl> <RepositoryUrl>https://lab.code-rove.com/gitea/tudor.stanciu/tuitio</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/ReleaseNotes.txt"))</PackageReleaseNotes> <PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/ReleaseNotes.txt"))</PackageReleaseNotes>
<Version>2.0.0</Version> <Version>2.1.0</Version>
<PackageIcon>logo.png</PackageIcon> <PackageIcon>logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<Company>Toodle HomeLab</Company> <Company>Toodle HomeLab</Company>

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}

View File

@ -11,11 +11,8 @@ namespace Tuitio.Api.Controllers
[Route("system")] [Route("system")]
public class SystemController : ControllerBase public class SystemController : ControllerBase
{ {
private readonly IMediator _mediator;
public SystemController(IMediator mediator) public SystemController(IMediator mediator)
{ {
_mediator = mediator;
} }
[AllowAnonymous] [AllowAnonymous]
@ -24,12 +21,5 @@ namespace Tuitio.Api.Controllers
{ {
return Ok($"Ping success. System datetime: {DateTime.Now}"); return Ok($"Ping success. System datetime: {DateTime.Now}");
} }
/*
Methods:
/version
/burn-token
/burn-all-tokens
*/
} }
} }

View File

@ -22,7 +22,7 @@ namespace Tuitio.Extensions
services.AddControllers(); services.AddControllers();
// MediatR // 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(RequestPreProcessorBehavior<,>));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPostProcessorBehavior<,>)); services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPostProcessorBehavior<,>));