Tuitio refactoring and account logout implementation
parent
874baa02e8
commit
e5854ef76b
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
{
|
{
|
||||||
private readonly IUserService _userService;
|
public record Command(string Token) : IRequest<Envelope<TokenAuthorizationResult>>;
|
||||||
private readonly IMapper _mapper;
|
|
||||||
private readonly ILogger<AuthorizeTokenHandler> _logger;
|
|
||||||
|
|
||||||
public AuthorizeTokenHandler(IUserService userService, IMapper mapper, ILogger<AuthorizeTokenHandler> logger)
|
public class CommandHandler : IRequestHandler<Command, Envelope<TokenAuthorizationResult>>
|
||||||
{
|
{
|
||||||
_userService = userService;
|
private readonly IUserService _userService;
|
||||||
_mapper = mapper;
|
private readonly IMapper _mapper;
|
||||||
_logger = logger;
|
private readonly ILogger<AuthorizeTokenHandler> _logger;
|
||||||
}
|
|
||||||
|
|
||||||
public Task<TokenCore> Handle(AuthorizeToken command, CancellationToken cancellationToken)
|
public CommandHandler(IUserService userService, IMapper mapper, ILogger<AuthorizeTokenHandler> logger)
|
||||||
{
|
|
||||||
var tokenCore = _userService.Authorize(command.Token);
|
|
||||||
if (tokenCore == null)
|
|
||||||
{
|
{
|
||||||
_logger.LogDebug($"Authorization failed for token '{command.Token}'.");
|
_userService = userService;
|
||||||
return null;
|
_mapper = mapper;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug($"Authorization succeeded for token '{command.Token}'.");
|
public Task<Envelope<TokenAuthorizationResult>> Handle(Command command, CancellationToken cancellationToken)
|
||||||
var tokenCoreResult = _mapper.Map<TokenCore>(tokenCore);
|
{
|
||||||
|
var token = _userService.Authorize(command.Token);
|
||||||
|
if (token == null)
|
||||||
|
{
|
||||||
|
_logger.LogDebug($"Authorization failed for token '{command.Token}'.");
|
||||||
|
var result = Envelope<TokenAuthorizationResult>.Error(EnvelopeStatus.UNAUTHORIZED);
|
||||||
|
return Task.FromResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
return Task.FromResult(tokenCoreResult);
|
_logger.LogDebug($"Authorization succeeded for token '{command.Token}'.");
|
||||||
|
var authorizationResult = new TokenAuthorizationResult(_mapper.Map<TokenCore>(token));
|
||||||
|
var envelope = Envelope<TokenAuthorizationResult>.Success(authorizationResult);
|
||||||
|
return Task.FromResult(envelope);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
|
||||||
|
|
||||||
using MediatR;
|
|
||||||
using Tuitio.PublishedLanguage.Events;
|
|
||||||
|
|
||||||
namespace Tuitio.Application.Commands
|
|
||||||
{
|
|
||||||
public class AuthenticateUser : IRequest<AuthenticateUserResult>
|
|
||||||
{
|
|
||||||
public string UserName { get; set; }
|
|
||||||
public string Password { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
|
||||||
|
|
||||||
using MediatR;
|
|
||||||
using Tuitio.PublishedLanguage.Dto;
|
|
||||||
|
|
||||||
namespace Tuitio.Application.Commands
|
|
||||||
{
|
|
||||||
public class AuthorizeToken : IRequest<TokenCore>
|
|
||||||
{
|
|
||||||
public string Token { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
@ -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>();
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
|
@ -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; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Tuitio.Domain.Models
|
|
||||||
{
|
|
||||||
public class TokenCore
|
|
||||||
{
|
|
||||||
public int UserId { get; set; }
|
|
||||||
public string UserName { get; set; }
|
|
||||||
public string FirstName { get; set; }
|
|
||||||
public string LastName { get; set; }
|
|
||||||
public string Email { get; set; }
|
|
||||||
public string ProfilePictureUrl { get; set; }
|
|
||||||
public string SecurityStamp { get; set; }
|
|
||||||
public string LockStamp { get; set; }
|
|
||||||
public Dictionary<string, string> Claims { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,13 +3,15 @@
|
||||||
using Tuitio.Domain.Entities;
|
using Tuitio.Domain.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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 application’s service collection. It contains two methods that provide a simple and convenient way for developers to handle authentication and authorization when communicating with Tuitio’s API.</Description>
|
<Description>Tuitio.Wrapper facilitates integration with a Tuitio instance in a .NET environment by registering a service called ITuitioService in the application’s service collection. It contains three methods that provide a simple and convenient way for developers to handle authentication and authorization when communicating with Tuitio’s API.</Description>
|
||||||
<PackageProjectUrl>https://lab.code-rove.com/gitea/tudor.stanciu/tuitio</PackageProjectUrl>
|
<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>
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<,>));
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue