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 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.

View File

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

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
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
{

View File

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

View File

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

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

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
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
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

View File

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

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

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

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
◾ 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>
<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>

View File

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

View File

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

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.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

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
◾ 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>
<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>
<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>

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")]
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
*/
}
}

View File

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