Tuitio refactoring and account logout implementation

master
Tudor Stanciu 2023-03-07 19:44:55 +02:00
parent 874baa02e8
commit e5854ef76b
41 changed files with 403 additions and 309 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,9 +8,9 @@
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>

View File

@ -0,0 +1,46 @@
// Copyright (c) 2020 Tudor Stanciu
using MediatR;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
using Tuitio.Application.Services;
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($"Authentication failed for user '{command.UserName}'.");
return Envelope<AccountLoginResult>.Error(EnvelopeStatus.BAD_CREDENTIALS);
}
_logger.LogDebug($"Authentication succeeded for user '{command.UserName}'.");
var token = new Token(loginResult.Raw, loginResult.Token.ExpiresIn);
var result = new AccountLoginResult(token);
return Envelope<AccountLoginResult>.Success(result);
}
}
}
}

View File

@ -0,0 +1,40 @@
// Copyright (c) 2020 Tudor Stanciu
using AutoMapper;
using MediatR;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
using Tuitio.Application.Services;
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);
_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

@ -1,42 +1,48 @@
// Copyright (c) 2020 Tudor Stanciu // Copyright (c) 2020 Tudor Stanciu
using AutoMapper; using AutoMapper;
using Tuitio.Application.Commands;
using Tuitio.Application.Services; using Tuitio.Application.Services;
using Tuitio.PublishedLanguage.Dto; using Tuitio.PublishedLanguage.Dto;
using MediatR; using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Tuitio.PublishedLanguage.Constants;
namespace Tuitio.Application.CommandHandlers namespace Tuitio.Application.CommandHandlers
{ {
public class AuthorizeTokenHandler : IRequestHandler<AuthorizeToken, TokenCore> public class AuthorizeTokenHandler
{
public record Command(string Token) : IRequest<Envelope<TokenAuthorizationResult>>;
public class CommandHandler : IRequestHandler<Command, Envelope<TokenAuthorizationResult>>
{ {
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly ILogger<AuthorizeTokenHandler> _logger; private readonly ILogger<AuthorizeTokenHandler> _logger;
public AuthorizeTokenHandler(IUserService userService, IMapper mapper, ILogger<AuthorizeTokenHandler> logger) public CommandHandler(IUserService userService, IMapper mapper, ILogger<AuthorizeTokenHandler> logger)
{ {
_userService = userService; _userService = userService;
_mapper = mapper; _mapper = mapper;
_logger = logger; _logger = logger;
} }
public Task<TokenCore> Handle(AuthorizeToken command, CancellationToken cancellationToken) public Task<Envelope<TokenAuthorizationResult>> Handle(Command command, CancellationToken cancellationToken)
{ {
var tokenCore = _userService.Authorize(command.Token); var token = _userService.Authorize(command.Token);
if (tokenCore == null) if (token == null)
{ {
_logger.LogDebug($"Authorization failed for token '{command.Token}'."); _logger.LogDebug($"Authorization failed for token '{command.Token}'.");
return null; var result = Envelope<TokenAuthorizationResult>.Error(EnvelopeStatus.UNAUTHORIZED);
return Task.FromResult(result);
} }
_logger.LogDebug($"Authorization succeeded for token '{command.Token}'."); _logger.LogDebug($"Authorization succeeded for token '{command.Token}'.");
var tokenCoreResult = _mapper.Map<TokenCore>(tokenCore); var authorizationResult = new TokenAuthorizationResult(_mapper.Map<TokenCore>(token));
var envelope = Envelope<TokenAuthorizationResult>.Success(authorizationResult);
return Task.FromResult(tokenCoreResult); return Task.FromResult(envelope);
}
} }
} }
} }

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

@ -12,10 +12,11 @@ namespace Tuitio.Application.Mappings
{ {
public MappingProfile() public MappingProfile()
{ {
CreateMap<models.Token, dto.Token>(); CreateMap<models.Token, dto.TokenCore>();
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

@ -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,12 +15,14 @@ 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;
_tokenService=tokenService;
_securityStore=securityStore; _securityStore=securityStore;
} }
@ -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

@ -8,6 +8,7 @@ namespace Tuitio.Application.Services
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

@ -2,12 +2,14 @@
using Tuitio.Domain.Models; using Tuitio.Domain.Models;
using System.Threading.Tasks; using System.Threading.Tasks;
using Tuitio.Domain.Models.Account;
namespace Tuitio.Application.Services namespace Tuitio.Application.Services
{ {
public interface IUserService public interface IUserService
{ {
Task<Token> Authenticate(string userName, string password); Task<LoginResult> Login(string userName, string password);
TokenCore Authorize(string token); Task<LogoutResult> Logout(string tokenRaw);
Token Authorize(string tokenRaw);
} }
} }

View File

@ -1,13 +1,14 @@
// 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.Stores;
using Tuitio.Domain.Abstractions;
using Tuitio.Domain.Entities;
using Tuitio.Domain.Models;
namespace Tuitio.Application.Services namespace Tuitio.Application.Services
{ {
@ -15,47 +16,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

@ -8,54 +8,69 @@ using Tuitio.Domain.Models;
using Tuitio.Domain.Repositories; using Tuitio.Domain.Repositories;
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Tuitio.Domain.Models.Account;
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);
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.UpdateUserAfterAuthentication(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

@ -11,11 +11,11 @@ using System.Threading.Tasks;
namespace Tuitio.Domain.Data.Repositories namespace Tuitio.Domain.Data.Repositories
{ {
class IdentityRepository : IIdentityRepository class UserRepository : IUserRepository
{ {
private readonly IdentityDbContext _dbContext; private readonly TuitioDbContext _dbContext;
public IdentityRepository(IdentityDbContext dbContext) public UserRepository(TuitioDbContext dbContext)
{ {
_dbContext = dbContext; _dbContext = dbContext;
} }
@ -28,29 +28,48 @@ namespace Tuitio.Domain.Data.Repositories
.FirstOrDefaultAsync(z => z.UserName == userName && z.Password == password); .FirstOrDefaultAsync(z => z.UserName == userName && z.Password == password);
} }
public async Task UpdateUserAfterAuthentication(AppUser user, Token token) public async Task UpdateUserAfterAuthentication(AppUser user, Token token, string tokenRaw)
{ {
var userToken = new UserToken() var userToken = new UserToken()
{ {
UserId = user.UserId, TokenId = token.TokenId,
Token = token.Raw, UserId = token.UserId,
ValidFrom = token.ValidFrom, Token = tokenRaw,
ValidUntil = token.ValidUntil ValidFrom = token.CreatedAt,
ValidUntil = token.CreatedAt.AddMilliseconds(token.ExpiresIn)
}; };
await _dbContext.AddAsync(userToken); await _dbContext.AddAsync(userToken);
user.LastLoginDate = DateTime.Now; user.LastLoginDate = DateTime.UtcNow;
_dbContext.Update(user);
//asta nu trebuie
//_dbContext.Update(user);
await _dbContext.SaveChangesAsync(); await _dbContext.SaveChangesAsync();
} }
public Task<UserToken[]> GetActiveTokens() public Task<UserToken[]> GetActiveTokens()
{ {
var currentDate = DateTime.Now; var currentDate = DateTime.UtcNow;
var query = _dbContext.UserTokens var query = _dbContext.UserTokens
.Where(z => z.ValidFrom <= currentDate && z.ValidUntil >= currentDate && (!z.Burnt.HasValue || z.Burnt.Value == false)); .Where(z => z.ValidFrom <= currentDate && z.ValidUntil >= currentDate);
// read all tokens, keep the valid ones and remove the expired ones.
return query.ToArrayAsync(); return query.ToArrayAsync();
} }
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,7 @@
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 UpdateUserAfterAuthentication(AppUser user, Token token, string tokenRaw);
Task<UserToken[]> GetActiveTokens(); Task<UserToken[]> GetActiveTokens();
Task RemoveToken(Guid tokenId);
} }
} }

View File

@ -2,10 +2,10 @@
namespace Tuitio.PublishedLanguage.Constants namespace Tuitio.PublishedLanguage.Constants
{ {
public struct AuthenticationStatus public struct EnvelopeStatus
{ {
public const string public const string
SUCCESS = "SUCCESS", BAD_CREDENTIALS = "BAD_CREDENTIALS",
BAD_CREDENTIALS = "BAD_CREDENTIALS"; 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 Status { get; }
public Envelope(T result, string status)
{
Result=result;
Status=status;
}
public static Envelope<T> Success(T result)
{
return new Envelope<T>(result, null);
}
public static Envelope<T> Error(string message)
{
return new Envelope<T>(null, message);
}
}
}

View File

@ -0,0 +1,10 @@
// Copyright (c) 2020 Tudor Stanciu
using System;
namespace Tuitio.PublishedLanguage.Dto
{
public record AccountLoginResult(Token Token);
public record AccountLogoutResult(int UserId, string UserName, DateTime LogoutDate);
public record TokenAuthorizationResult(TokenCore TokenCore);
}

View File

@ -1,13 +1,7 @@
// Copyright (c) 2020 Tudor Stanciu // Copyright (c) 2020 Tudor Stanciu
using System;
namespace Tuitio.PublishedLanguage.Dto namespace Tuitio.PublishedLanguage.Dto
{ {
public class Token // move this in result
{ public record Token(string Raw, double ExpiresIn);
public string Raw { get; set; }
public DateTime ValidFrom { get; set; }
public DateTime ValidUntil { 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

@ -5,7 +5,7 @@ namespace Tuitio.Wrapper.Constants
internal struct ApiRoutes internal struct ApiRoutes
{ {
public const string public const string
Authentication = "identity/authenticate?UserName={0}&Password={1}", Authentication = "account/login?UserName={0}&Password={1}",
Authorization = "identity/authorize?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

@ -5,9 +5,9 @@ using System.Threading.Tasks;
namespace Tuitio.Wrapper.Services namespace Tuitio.Wrapper.Services
{ {
public interface IIdentityService public interface ITuitioService
{ {
Task<Token> Authenticate(string userName, string password); Task<Token> Authenticate(string userName, string password);
Task<TokenCore> Authorize(string token); Task<TokenAuthorizationResult> Authorize(string token);
} }
} }

View File

@ -11,12 +11,12 @@ using System.Threading.Tasks;
namespace Tuitio.Wrapper.Services namespace Tuitio.Wrapper.Services
{ {
internal class IdentityService : IIdentityService internal class TuitioService : ITuitioService
{ {
private const string _contentType = "application/json"; private const string _contentType = "application/json";
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
public IdentityService(HttpClient httpClient, ServiceConfiguration configuration) public TuitioService(HttpClient httpClient, ServiceConfiguration configuration)
{ {
httpClient.BaseAddress = new Uri(configuration.BaseAddress); httpClient.BaseAddress = new Uri(configuration.BaseAddress);
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(_contentType)); httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(_contentType));
@ -31,10 +31,10 @@ namespace Tuitio.Wrapper.Services
return result; return result;
} }
public async Task<TokenCore> Authorize(string token) public async Task<TokenAuthorizationResult> Authorize(string token)
{ {
var route = string.Format(ApiRoutes.Authorization, token); var route = string.Format(ApiRoutes.Authorization, token);
var result = await _httpClient.ExecutePostRequest<TokenCore>(route); var result = await _httpClient.ExecutePostRequest<TokenAuthorizationResult>(route);
return result; return result;
} }
} }

View File

@ -2,7 +2,7 @@
<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>

View File

@ -0,0 +1,38 @@
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 result = await _mediator.Send(token);
if (result != null)
return Ok(result);
else
return BadRequest();
}
}
}

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

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