From b697a4b357d920b53c0a8f3a0d7266330e4a3e3e Mon Sep 17 00:00:00 2001 From: Wvader <34067397+wvader@users.noreply.github.com> Date: Sun, 18 Sep 2022 02:00:24 +0100 Subject: [PATCH] Redis working --- .../Extensions/ModelBuilderExtensions.cs | 11 -- BlueWest.Api/Context/SessionDbContext.cs | 30 ---- BlueWest.Api/Controllers/AuthController.cs | 73 +++------ BlueWest.Api/Controllers/CountryController.cs | 2 +- BlueWest.Api/Services/IndexCreationDevice.cs | 33 ---- BlueWest.Api/Session/ISessionCache.cs | 48 ++++++ BlueWest.Api/Session/ISessionManager.cs | 16 -- BlueWest.Api/Session/SessionConstants.cs | 12 ++ BlueWest.Api/Session/SessionDataService.cs | 108 +++++++++++++ BlueWest.Api/Session/SessionManager.cs | 97 ----------- BlueWest.Api/Startup.cs | 2 +- BlueWest.Api/StartupExtensions.cs | 76 ++++++--- BlueWest.Api/Users/Auth/AuthManager.cs | 150 +++++++++++------- .../Users/Auth/Crypto/BaseCryptoItem.cs | 11 +- .../Users/Auth/Crypto/JwtIssuerOptions.cs | 2 +- BlueWest.Api/Users/Auth/Crypto/SHA_512.cs | 4 +- BlueWest.Api/Users/Auth/IAuthManager.cs | 26 ++- BlueWest.Api/Users/Models/LoginRequest.cs | 9 ++ BlueWest.Api/Users/UserRepository.cs | 35 ++-- BlueWest.Api/appsettings.json | 11 +- BlueWest.Api/config.json | 1 + .../SessionToken/SessionToken.cs | 21 ++- .../SessionToken/SessionTokenUnique.cs | 5 +- BlueWest.sln | 1 + data/appendonly.aof | 0 docker-compose.db.only.yml | 29 ++++ docker-compose.yml | 33 ++-- 27 files changed, 452 insertions(+), 394 deletions(-) delete mode 100644 BlueWest.Api/Context/SessionDbContext.cs delete mode 100644 BlueWest.Api/Services/IndexCreationDevice.cs create mode 100644 BlueWest.Api/Session/ISessionCache.cs delete mode 100644 BlueWest.Api/Session/ISessionManager.cs create mode 100644 BlueWest.Api/Session/SessionConstants.cs create mode 100644 BlueWest.Api/Session/SessionDataService.cs delete mode 100644 BlueWest.Api/Session/SessionManager.cs create mode 100644 data/appendonly.aof create mode 100644 docker-compose.db.only.yml diff --git a/BlueWest.Api/Context/Extensions/ModelBuilderExtensions.cs b/BlueWest.Api/Context/Extensions/ModelBuilderExtensions.cs index d76dfac..4918f41 100644 --- a/BlueWest.Api/Context/Extensions/ModelBuilderExtensions.cs +++ b/BlueWest.Api/Context/Extensions/ModelBuilderExtensions.cs @@ -87,11 +87,6 @@ namespace BlueWest.WebApi.EF.Model builder.Entity().ToTable("RoleClaims"); builder.Entity().ToTable("UserRole"); - // Session Token - builder.Entity() - .HasOne(b => b.User) - .WithMany(x => x.SessionToken) - .HasForeignKey(x => x.UserId); // Session Token Primary Key builder.Entity(b => @@ -105,12 +100,6 @@ namespace BlueWest.WebApi.EF.Model .WithMany(x => x.SessionDatas) .HasForeignKey(x => x.UserId); - // Session Data - builder.Entity() - .HasOne(b => b.SessionToken) - .WithOne(x => x.SessionData); - - // Session Data Primary Key builder.Entity(b => diff --git a/BlueWest.Api/Context/SessionDbContext.cs b/BlueWest.Api/Context/SessionDbContext.cs deleted file mode 100644 index 8a2eaba..0000000 --- a/BlueWest.Api/Context/SessionDbContext.cs +++ /dev/null @@ -1,30 +0,0 @@ -using BlueWest.Data.Application; -using BlueWest.WebApi.EF.Model; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.InMemory; - -namespace BlueWest.WebApi.Context -{ - - public class SessionDbContext : DbContext - { - /// - /// CountryDbContext Constructor. - /// - /// - - public SessionDbContext(DbContextOptions options) : base(options) - { - Database.EnsureCreated(); - } - - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - modelBuilder.ConfigureCurrentDbModel(); - } - - public DbSet SessionTokens { get; set; } - } -} diff --git a/BlueWest.Api/Controllers/AuthController.cs b/BlueWest.Api/Controllers/AuthController.cs index 7884191..156cabb 100644 --- a/BlueWest.Api/Controllers/AuthController.cs +++ b/BlueWest.Api/Controllers/AuthController.cs @@ -1,14 +1,10 @@ using System; using System.Security.Claims; using System.Threading.Tasks; -using BlueWest.Cryptography; -using BlueWest.Data.Application; using BlueWest.WebApi.Context.Users; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; @@ -19,29 +15,24 @@ namespace BlueWest.WebApi.Controllers; /// [ApiController] [Route("api/[controller]")] - - [Authorize(Policy = "ApiUser")] + [Authorize(Policy = SessionConstants.ApiNamePolicy)] /*[EnableCors(Constants.CorsPolicyName)]*/ public class AuthController : Controller { private readonly IAuthManager _authManager; private readonly IUserManager _userManager; - private readonly ISessionManager _sessionManager; /// /// /// /// /// - public AuthController( IAuthManager authManager, IUserManager userManager, ISessionManager sessionManager) + public AuthController( IAuthManager authManager, IUserManager userManager) { _authManager = authManager; _userManager = userManager; - _sessionManager = sessionManager; } - - /// /// Signup user /// @@ -62,64 +53,49 @@ namespace BlueWest.WebApi.Controllers; /// /// [AllowAnonymous] - [HttpPost("token")] - public async Task> GetTokenAsync(LoginRequest loginViewModel) + [HttpPost("login")] + public async Task> GetSessionToken(LoginRequest loginViewModel) { - var (success, sessionToken, token) = await _authManager.GetToken(loginViewModel); + var (success, sessionToken, identity) = await _authManager.GetSessionTokenId(loginViewModel); if (success) { - return Ok(new {sessionToken, token}); + return Ok(new {sessionToken}); } return Problem(); } - - + /// - /// Check if user is logged in + /// Gets a bearer token /// + /// /// - [HttpGet("isLoggedIn")] - - public ActionResult IsLoggedIn() + [AllowAnonymous] + [HttpPost("bearer")] + public async Task> GetBearerBySessionId(string sessionId) { - var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme); - - if (identity.IsAuthenticated) + var (success, bearer) = await _authManager.GetBearerTokenBySessionTokenId(sessionId); + + if (success) { - return Ok(true); + return Ok(new {bearer}); + } - - return Ok(false); + return new UnauthorizedObjectResult(new {message = "The provided sessionId didn't return a valid Token."}); } - /// - /// Checks if the session is authorized - /// - /// - /// - [HttpGet("isAuthorized")] - - public ActionResult IsAuthorized(string hash) - { - var isAuthorized = _sessionManager.IsAuthorized(hash); - - return Ok(isAuthorized ? new {authenticated = true} : new {authenticated = false}); - } - - /// /// Do Cookie based login. /// /// /// - [AllowAnonymous] + /*[AllowAnonymous] [HttpPost("login")] - public async Task DoLoginAsync(LoginRequest loginDto) + public async Task DoLoginByCookie(LoginRequest loginDto) { - var (success, identity, sessionToken) = await _authManager.DoLogin(loginDto); + var (success, sessionToken, identity) = await _authManager.GetSessionTokenId(loginDto); if (success) { @@ -129,23 +105,22 @@ namespace BlueWest.WebApi.Controllers; new AuthenticationProperties { IsPersistent = true, - ExpiresUtc = DateTime.UtcNow.AddDays(1) + ExpiresUtc = DateTime.UtcNow.Add(SessionConstants.DefaultValidForSpan) }); return Ok(new {authenticated = true, sessionToken}); } return new ForbidResult(CookieAuthenticationDefaults.AuthenticationScheme); - } + }*/ /// /// Do Cookie based logout /// - /// /// [AllowAnonymous] [HttpPost("logout")] - public async Task DoLogoutAsync() + public async Task DoCookieLogoutAsync() { await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); } diff --git a/BlueWest.Api/Controllers/CountryController.cs b/BlueWest.Api/Controllers/CountryController.cs index d82e63c..a000cff 100644 --- a/BlueWest.Api/Controllers/CountryController.cs +++ b/BlueWest.Api/Controllers/CountryController.cs @@ -18,7 +18,7 @@ namespace BlueWest.WebApi.Controllers /// [ApiController] [Route("[controller]")] - [Authorize(Policy = "ApiUser")] + [Authorize(Policy = SessionConstants.ApiNamePolicy)] [EnableCors(Constants.CorsPolicyName)] // [Authorize(Roles = "Administrator")] public class CountryController : ControllerBase diff --git a/BlueWest.Api/Services/IndexCreationDevice.cs b/BlueWest.Api/Services/IndexCreationDevice.cs deleted file mode 100644 index 24329b4..0000000 --- a/BlueWest.Api/Services/IndexCreationDevice.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using BlueWest.Data.Application; -using Microsoft.Extensions.Hosting; -using Redis.OM; - -namespace BlueWest.WebApi -{ - public class IndexCreationDevice : IHostedService - { - private readonly RedisConnectionProvider _provider; - - /// - /// Index Creation Device - /// - /// - public IndexCreationDevice(RedisConnectionProvider provider) - { - _provider = provider; - } - - /// - public async Task StartAsync(CancellationToken cancellationToken) - { - await _provider.Connection.CreateIndexAsync(typeof(SessionToken)); } - - /// - public Task StopAsync(CancellationToken cancellationToken) - { - return Task.CompletedTask; } - } -} - diff --git a/BlueWest.Api/Session/ISessionCache.cs b/BlueWest.Api/Session/ISessionCache.cs new file mode 100644 index 0000000..2d55a69 --- /dev/null +++ b/BlueWest.Api/Session/ISessionCache.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; +using BlueWest.Data.Application; +using BlueWest.WebApi.Context.Users; +using Microsoft.Extensions.Hosting; + +namespace BlueWest.WebApi +{ + /// + /// Methods for handling session cache data. + /// + public interface ISessionCache : IHostedService + { + /// + /// Gets a Bearer By Access Token Id + /// + /// + /// + Task GetBearerByAccessTokenId(string sessionTokenId); + + /// + /// Gets a Session Token by Id. + /// + /// + /// + Task GetSessionTokenByIdAsync(string tokenId); + /// + /// Create a new session token + /// + /// + Task AddSessionToken(SessionToken token); + + /// + /// Save Cache + /// + /// + Task SaveAsync(); + + + /// + /// Save Cache + /// + /// + void Save(); + + + } +} + diff --git a/BlueWest.Api/Session/ISessionManager.cs b/BlueWest.Api/Session/ISessionManager.cs deleted file mode 100644 index 2017751..0000000 --- a/BlueWest.Api/Session/ISessionManager.cs +++ /dev/null @@ -1,16 +0,0 @@ -using BlueWest.Data.Application; -using BlueWest.WebApi.Context.Users; - -namespace BlueWest.WebApi -{ - public interface ISessionManager - { - bool IsAuthorized(string tokenHash); - - SessionToken GetSessionToken(string hash, ApplicationUser applicationUser); - - SessionToken GetSessionToken(LoginRequest loginRequest, ApplicationUser user); - - } -} - diff --git a/BlueWest.Api/Session/SessionConstants.cs b/BlueWest.Api/Session/SessionConstants.cs new file mode 100644 index 0000000..012ea1c --- /dev/null +++ b/BlueWest.Api/Session/SessionConstants.cs @@ -0,0 +1,12 @@ +using System; + +namespace BlueWest.WebApi +{ + internal static class SessionConstants + { + + public static TimeSpan DefaultValidForSpan = TimeSpan.FromHours(24); + public const string ApiNamePolicy = "ApiUser"; + } +} + diff --git a/BlueWest.Api/Session/SessionDataService.cs b/BlueWest.Api/Session/SessionDataService.cs new file mode 100644 index 0000000..90ac343 --- /dev/null +++ b/BlueWest.Api/Session/SessionDataService.cs @@ -0,0 +1,108 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using BlueWest.Cryptography; +using BlueWest.Data.Application; +using Microsoft.Extensions.Hosting; +using Redis.OM; +using Redis.OM.Searching; + +namespace BlueWest.WebApi.Session +{ + /// + /// Session Provider Context + /// + public sealed class SessionDataService : IHostedService, ISessionCache + { + private readonly RedisConnectionProvider _provider; + private RedisCollection _sessionTokens; + /// + /// Index Creation Device + /// + /// Redis connection + public SessionDataService( + RedisConnectionProvider provider) + { + _provider = provider; + _sessionTokens = (RedisCollection)provider.RedisCollection(); + } + + /// + /// Empty constructor + /// + public SessionDataService() { } + + /// + /// Get a session token by the respective Id. + /// + /// + /// + public async Task GetSessionTokenByIdAsync(string tokenId) + { + return await _sessionTokens.Where(x => x.Id == tokenId) + .FirstOrDefaultAsync(); + } + + /// + /// Create a new session token + /// + /// + public async Task AddSessionToken(SessionToken token) + { + await _sessionTokens.InsertAsync(token); + } + + /// + public async Task SaveAsync() + { + await _sessionTokens.SaveAsync(); + } + + /// + /// Save session data + /// + public void Save() + { + _sessionTokens.Save(); + } + + + /// + /// Gets a Bearer By Access Token Id + /// + /// + public async Task GetBearerByAccessTokenId(string sessionTokenId) + { + var accessToken = await _sessionTokens.Where(t => t.Id == sessionTokenId) + .FirstOrDefaultAsync(); + + if (accessToken == null) return string.Empty; + + if (accessToken.IsValid) + { + var createdDate = DateTime.UnixEpoch.AddMilliseconds(accessToken.CreatedDate); + if (createdDate.AddMilliseconds(accessToken.ValidFor) < DateTime.Now) + { + accessToken.IsValid = false; + } + } + + await _sessionTokens.SaveAsync(); + + return accessToken.IsValid ? accessToken.AccessToken : string.Empty; + } + + /// + public async Task StartAsync(CancellationToken cancellationToken) + { + await _provider.Connection.CreateIndexAsync(typeof(SessionToken)); + } + + /// + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} + diff --git a/BlueWest.Api/Session/SessionManager.cs b/BlueWest.Api/Session/SessionManager.cs deleted file mode 100644 index 45e6a25..0000000 --- a/BlueWest.Api/Session/SessionManager.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Linq; -using BlueWest.Cryptography; -using BlueWest.Data.Application; -using BlueWest.WebApi.Context; -using BlueWest.WebApi.Context.Users; - -namespace BlueWest.WebApi -{ - internal class SessionManager : ISessionManager - { - private readonly SessionDbContext _dbContex; - private readonly IHasher _hasher; - - public SessionManager(SessionDbContext sessionDbContext, IHasher hasher) - { - _dbContex = sessionDbContext; - _hasher = hasher; - } - - - /// - /// Check if token is authorized - /// - /// - /// - public bool IsAuthorized(string hash) - { - var sessionToken = _dbContex.SessionTokens.FirstOrDefault(x => x.Token == hash); - - if (sessionToken is not {IsValid: true}) return false; - - var expirationDate = sessionToken.CreatedDate.Add(sessionToken.ValidFor); - - if (expirationDate >= DateTime.Now) - { - return true; - } - - return false; - - } - - /// - /// Gets a new or existing session token - /// - /// - /// - /// - public SessionToken GetSessionToken(string hash, ApplicationUser applicationUser) - { - var sessionToken = _dbContex.SessionTokens.FirstOrDefault(x => x.Token == hash); - - if (sessionToken == null) - { - // Create token; - var newToken = new SessionToken - { - Token = hash, - CreatedDate = DateTime.Now, - ValidFor = TimeSpan.FromDays(1), - UserId = applicationUser.Id - }; - - _dbContex.SessionTokens.Add(newToken); - var result = _dbContex.SaveChanges() >= 0; - - return !result ? null : newToken; - } - - var expirationDate = sessionToken.CreatedDate.Add(sessionToken.ValidFor); - - if (expirationDate >= DateTime.Now) - { - return sessionToken; - } - - return null; - - } - - /// - /// Gets a session token for the user following a login request - /// - /// - /// - /// - public SessionToken GetSessionToken(LoginRequest loginRequest, ApplicationUser user) - { - var content = $"{loginRequest.Uuid}|{user.Email}"; - var hash = _hasher.CreateHash(content, BaseCryptoItem.HashAlgorithm.SHA2_512); - var sessionToken = GetSessionToken(hash, user); - return sessionToken; - } - } -} - diff --git a/BlueWest.Api/Startup.cs b/BlueWest.Api/Startup.cs index 99711cb..cbc6334 100644 --- a/BlueWest.Api/Startup.cs +++ b/BlueWest.Api/Startup.cs @@ -153,7 +153,7 @@ namespace BlueWest.WebApi switch (allowedDatabase) { case "mysql": - services.PrepareMySqlDatabasePool(_configuration, _environment); + services.PrepareMySqlDatabasePool(_configuration, _environment, configuration); break; case "sqlite": diff --git a/BlueWest.Api/StartupExtensions.cs b/BlueWest.Api/StartupExtensions.cs index 11ff6ee..229b410 100644 --- a/BlueWest.Api/StartupExtensions.cs +++ b/BlueWest.Api/StartupExtensions.cs @@ -5,21 +5,19 @@ using BlueWest.Cryptography; using BlueWest.WebApi.Context; using BlueWest.WebApi.Context.Users; using BlueWest.WebApi.EF; -using Microsoft.AspNetCore.Authentication; +using BlueWest.WebApi.Session; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; -using Microsoft.OpenApi.Models; +using Redis.OM; namespace BlueWest.WebApi { @@ -39,13 +37,36 @@ namespace BlueWest.WebApi private static DbContextOptionsBuilder GetMySqlSettings( this DbContextOptionsBuilder optionsBuilder, IConfiguration configuration, + IConfigurationRoot configurationRoot, IWebHostEnvironment environment) { var sqlVersion = GetMySqlServerVersion(8, 0, 11); + + // Docker / No-Docker + var startupMode = configurationRoot["mode"]; + + string mySqlConnectionString = String.Empty; + + if (startupMode == "docker") + { + var config = configuration.Get(); + if(config != null) mySqlConnectionString = config.MySql; + } + else + { + var config = configuration.Get(); + if(config != null) mySqlConnectionString = config.MySql; + } + + if (mySqlConnectionString == string.Empty) + { + throw new InvalidOperationException("Fatal error: MySQL Connection string is empty."); + } + optionsBuilder .UseMySql( - configuration.GetConnectionString("DockerMySQL"), + mySqlConnectionString, sqlVersion) .UseMySql(sqlVersion, builder => @@ -74,16 +95,19 @@ namespace BlueWest.WebApi /// /// public static IServiceCollection PrepareMySqlDatabasePool(this IServiceCollection serviceCollection, - IConfiguration configuration, IWebHostEnvironment environment) + IConfiguration configuration, IWebHostEnvironment environment, IConfigurationRoot configurationRoot) { return serviceCollection - .AddDbContextPool(options => options.GetMySqlSettings(configuration, environment)) - .AddDbContextPool(options => options.GetMySqlSettings(configuration, environment)) - .AddDbContextPool(options => options.GetMySqlSettings(configuration, environment)) - .AddDbContextPool(options => options.GetMySqlSettings(configuration, environment)) + .AddDbContextPool(options => + options.GetMySqlSettings(configuration, configurationRoot, environment)) + .AddDbContextPool(options => + options.GetMySqlSettings(configuration, configurationRoot, environment)) + .AddDbContextPool(options => + options.GetMySqlSettings(configuration, configurationRoot, environment)) + .AddDbContextPool(options => + options.GetMySqlSettings(configuration, configurationRoot, environment)) .AddDbContextPool(options => - options.GetMySqlSettings(configuration, environment)) - .AddDbContextPool(options => options.UseInMemoryDatabase("_s")); + options.GetMySqlSettings(configuration, configurationRoot, environment)); } /// @@ -103,22 +127,23 @@ namespace BlueWest.WebApi .AddDbContextPool(options => options.UseSqlite(sqliteConString)) .AddDbContextPool(options => options.UseSqlite(sqliteConString)) .AddDbContextPool(options => options.UseSqlite(sqliteConString)) - .AddDbContextPool(options => options.UseSqlite(sqliteConString)) - .AddDbContextPool(options => options.UseInMemoryDatabase("_s")); + .AddDbContextPool(options => options.UseSqlite(sqliteConString)); } internal static IServiceCollection AddAuthServerServices(this IServiceCollection services, IConfiguration configuration , IWebHostEnvironment environment) { - services.AddScoped() + + services + .AddSingleton(new RedisConnectionProvider("redis://redisinstance:6379")) + .AddScoped() .AddScoped() - .AddSingleton() - .AddScoped() + .AddHostedService() + .AddSingleton() .AddScoped() .AddScoped() .AddScoped() - .AddScoped(); // Database Context and Swagger @@ -201,7 +226,7 @@ namespace BlueWest.WebApi // api user claim policy services.AddAuthorization(options => { - options.AddPolicy("ApiUser", + options.AddPolicy(SessionConstants.ApiNamePolicy, policy => policy.RequireClaim(Context.Users.Constants.JwtClaimIdentifiers.Rol, Context.Users.Constants.JwtClaims.ApiAccess)); @@ -230,6 +255,15 @@ namespace BlueWest.WebApi return services; } - } -} \ No newline at end of file +} + + +internal class BlueWestConnectionString +{ + public string Redis { get; set; } + public string MySql { get; set; } +} +internal class ConnectionStringDocker : BlueWestConnectionString { } +internal class ConnectionStringNoDocker : BlueWestConnectionString { } + diff --git a/BlueWest.Api/Users/Auth/AuthManager.cs b/BlueWest.Api/Users/Auth/AuthManager.cs index eb77915..8943eca 100644 --- a/BlueWest.Api/Users/Auth/AuthManager.cs +++ b/BlueWest.Api/Users/Auth/AuthManager.cs @@ -1,92 +1,130 @@ using System; using System.Security.Claims; -using System.Threading; using System.Threading.Tasks; using BlueWest.Cryptography; using BlueWest.Data.Application; +using Duende.IdentityServer.Extensions; using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; -namespace BlueWest.WebApi.Context.Users; - -internal class AuthManager : IAuthManager +namespace BlueWest.WebApi.Context.Users { + internal class AuthManager : IAuthManager + { private readonly ApplicationUserManager _userManager; - private readonly UserRepository _usersRepo; - private readonly ISessionManager _sessionManager; private readonly IHasher _hasher; private readonly IJwtFactory _jwtFactory; - + private readonly ISessionCache _sessionCache; + /// /// Auth manager constructor /// /// /// - /// /// + /// public AuthManager( - ApplicationUserManager userManager, - IHasher hasher, - UserRepository usersRepo, - ISessionManager sessionManager, - IJwtFactory jwtFactory) + ApplicationUserManager userManager, + IHasher hasher, + IJwtFactory jwtFactory, + ISessionCache sessionCache) { _userManager = userManager; _hasher = hasher; - _usersRepo = usersRepo; _jwtFactory = jwtFactory; - _sessionManager = sessionManager; + _sessionCache = sessionCache; } - public async Task<(bool, ClaimsIdentity, SessionTokenUnique)> DoLogin(LoginRequest loginRequest) + private async Task GetSessionToken(LoginRequest loginRequest) + { + var uuid = loginRequest.GetUuid(); + var hashUuid = GetHashFromUuid(uuid); + var sessionToken = await _sessionCache.GetSessionTokenByIdAsync(hashUuid); + if (sessionToken != null) return sessionToken; + return null; + } + + private string GetHashFromUuid(string uuid) + { + return _hasher.CreateHash(uuid, BaseCryptoItem.HashAlgorithm.SHA2_512); + } + + private SessionToken GetNewSessionToken(LoginRequest loginRequest, ApplicationUser user, string token) + { + long unixTime = ((DateTimeOffset)DateTimeOffset.Now).ToUnixTimeMilliseconds();; + var newToken = new SessionToken + { + Id = GetHashFromUuid(loginRequest.GetUuid()), + UserId = user.Id, + CreatedDate = unixTime, + IsValid = true, + AccessToken = token + }; + return newToken; + } + + public bool SessionTokenIsValid(SessionToken token) + { + var nowMilliseconds = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + + var isExpired = token.CreatedDate + token.ValidFor > nowMilliseconds; + + if (isExpired) + { + token.IsValid = false; + _sessionCache.SaveAsync(); + } + + return token.IsValid; + } + + public async Task<(bool, string, ClaimsIdentity)> GetSessionTokenId(LoginRequest loginRequest) { var user = await _userManager.FindByEmailAsync(loginRequest.Email); - if (user != null) + if (user == null) return (false, string.Empty, null); + + if (!await _userManager.CheckPasswordAsync(user, loginRequest.Password)) return (false, string.Empty, null); + + var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme); + identity.AddClaim(new Claim(ClaimTypes.Email, user.Email)); + + // Session + var sessionToken = await GetSessionToken(loginRequest); + + if (sessionToken != null) { - if(await _userManager.CheckPasswordAsync(user, loginRequest.Password)) + if (SessionTokenIsValid(sessionToken)) { - // Identity - var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme); - identity.AddClaim(new Claim(ClaimTypes.Email, user.Email)); - - // Session - var sessionToken = _sessionManager.GetSessionToken(loginRequest, user); - var sessionResponse = new SessionTokenUnique(sessionToken); - return (true, identity, sessionResponse); + return (true, sessionToken.Id, identity); } } - return (false, null, null); + var (success, bearerToken) = await GenerateBearerToken(identity, user); + + var newSessionToken = GetNewSessionToken(loginRequest, user, bearerToken); + await _sessionCache.AddSessionToken(newSessionToken); + + return (success, newSessionToken.Id, identity); } - - /// - public async Task<(bool, SessionTokenUnique, AccessToken)> GetToken(LoginRequest loginRequest) + + private async Task<(bool, string)> GenerateBearerToken(ClaimsIdentity identity, ApplicationUser user) { - if (!string.IsNullOrEmpty(loginRequest.Email) && !string.IsNullOrEmpty(loginRequest.Password)) - { - var user = await _userManager.FindByEmailAsync(loginRequest.Email); - if (user != null) - { - if (await VerifyLoginByEmailAsync(loginRequest.Email,loginRequest.Password)) - { - await _usersRepo.UpdateAsync(user, CancellationToken.None); - // Session - var sessionToken = _sessionManager.GetSessionToken(loginRequest, user); - var sessionResponse = new SessionTokenUnique(sessionToken); - - var token = await _jwtFactory.GenerateEncodedToken(user.Id, user.UserName); - var completed = await _userManager.SetAuthenticationTokenAsync(user, "ApiUser", "ApiUser", token.Token); - - return (completed == IdentityResult.Success, sessionResponse, token); - } - } - } - - return (false, null, null); + var jwtToken = await _jwtFactory.GenerateEncodedToken(user.Id, user.UserName); + var completed = await _userManager.SetAuthenticationTokenAsync(user, SessionConstants.ApiNamePolicy, + SessionConstants.ApiNamePolicy, jwtToken.Token); + return (completed == IdentityResult.Success, jwtToken.Token); } - + + + public async Task<(bool, string)> GetBearerTokenBySessionTokenId(string sessionId) + { + if (sessionId.IsNullOrEmpty()) return (false, string.Empty); + var bearer = await _sessionCache.GetBearerByAccessTokenId(sessionId); + return (bearer != string.Empty, bearer); + } + + /// public async Task VerifyLoginByEmailAsync(string email, string password) { @@ -94,13 +132,13 @@ internal class AuthManager : IAuthManager if (user == null) { - return false; + return false; } return await _userManager.CheckPasswordAsync(user, password); } - private RegisterViewModel FromSignupToUser(RegisterViewModel signupDto) + private RegisterViewModel FromSignupToUser(RegisterViewModel signupDto) { var pwd = signupDto.Password; var hash = _hasher.CreateHash(pwd, BaseCryptoItem.HashAlgorithm.SHA3_512); @@ -113,5 +151,5 @@ internal class AuthManager : IAuthManager RegisterViewModel userToCreate = FromSignupToUser(userSignupDto); return await _userManager.CreateAsync(userToCreate.ToUser()); } - + } } \ No newline at end of file diff --git a/BlueWest.Api/Users/Auth/Crypto/BaseCryptoItem.cs b/BlueWest.Api/Users/Auth/Crypto/BaseCryptoItem.cs index 103159a..7494398 100644 --- a/BlueWest.Api/Users/Auth/Crypto/BaseCryptoItem.cs +++ b/BlueWest.Api/Users/Auth/Crypto/BaseCryptoItem.cs @@ -80,18 +80,19 @@ namespace BlueWest.Cryptography keyIndex = null; trimmedCipherText = cipherText; - if (cipherText.Length <= 5 || cipherText[0] != '[') return; + if (cipherText.Length <= 5 ) return; - var cipherInfo = cipherText.Substring(1, cipherText.IndexOf(']') - 1).Split(","); + var cipherInfo = cipherText[0].ToString(); - if (int.TryParse(cipherInfo[0], out var foundAlgorithm)) + if (int.TryParse(cipherInfo, out int foundAlgorithm)) { algorithm = foundAlgorithm; } - if (cipherInfo.Length == 2 && int.TryParse(cipherInfo[1], out var foundKeyIndex)) + if (int.TryParse(cipherInfo, out int foundKeyIndex)) keyIndex = foundKeyIndex; - trimmedCipherText = cipherText.Substring(cipherText.IndexOf(']') + 1); + + trimmedCipherText = cipherText.Substring(cipherText.IndexOf(cipherInfo, StringComparison.Ordinal) + 1); } } } \ No newline at end of file diff --git a/BlueWest.Api/Users/Auth/Crypto/JwtIssuerOptions.cs b/BlueWest.Api/Users/Auth/Crypto/JwtIssuerOptions.cs index 530e5a5..a4b9572 100644 --- a/BlueWest.Api/Users/Auth/Crypto/JwtIssuerOptions.cs +++ b/BlueWest.Api/Users/Auth/Crypto/JwtIssuerOptions.cs @@ -39,7 +39,7 @@ internal class JwtIssuerOptions /// /// Set the timespan the token will be valid for (default is 120 min) /// - public TimeSpan ValidFor { get; set; } = TimeSpan.FromMinutes(120); + public TimeSpan ValidFor { get; set; } = SessionConstants.DefaultValidForSpan; /// /// "jti" (JWT ID) Claim (default ID is a GUID) diff --git a/BlueWest.Api/Users/Auth/Crypto/SHA_512.cs b/BlueWest.Api/Users/Auth/Crypto/SHA_512.cs index 24d0759..e2523a6 100644 --- a/BlueWest.Api/Users/Auth/Crypto/SHA_512.cs +++ b/BlueWest.Api/Users/Auth/Crypto/SHA_512.cs @@ -26,11 +26,11 @@ namespace BlueWest.Cryptography if (storeSalt) { - hash = $"[{(int)HashAlgorithm.SHA3_512}]{salt}{asString}"; + hash = $"{(int)HashAlgorithm.SHA3_512}{salt}{asString}"; return hash; } - hash = $"[{(int)HashAlgorithm.SHA3_512}]{asString}"; + hash = $"{(int)HashAlgorithm.SHA3_512}{asString}"; return hash; } diff --git a/BlueWest.Api/Users/Auth/IAuthManager.cs b/BlueWest.Api/Users/Auth/IAuthManager.cs index d775f43..7f4f54b 100644 --- a/BlueWest.Api/Users/Auth/IAuthManager.cs +++ b/BlueWest.Api/Users/Auth/IAuthManager.cs @@ -17,27 +17,19 @@ public interface IAuthManager /// /// Task CreateUserAsync(RegisterViewModel registerViewModel); - - /// - /// VerifyLoginAsync - /// - /// - /// - /// - Task VerifyLoginByEmailAsync(string email, string password); - - /// - /// GetToken - /// - /// - /// - Task<(bool, SessionTokenUnique, AccessToken)> GetToken(LoginRequest loginRequest); - + /// /// Does Login /// /// /// - Task<(bool, ClaimsIdentity, SessionTokenUnique)> DoLogin(LoginRequest loginRequest); + public Task<(bool, string, ClaimsIdentity)> GetSessionTokenId(LoginRequest loginRequest); + + /// + /// Gets a valid bearer token by the session id + /// + /// + /// + Task<(bool, string)> GetBearerTokenBySessionTokenId(string sessionId); } \ No newline at end of file diff --git a/BlueWest.Api/Users/Models/LoginRequest.cs b/BlueWest.Api/Users/Models/LoginRequest.cs index 7dea8b4..02f9fa9 100644 --- a/BlueWest.Api/Users/Models/LoginRequest.cs +++ b/BlueWest.Api/Users/Models/LoginRequest.cs @@ -25,6 +25,15 @@ namespace BlueWest.WebApi.Context.Users [Required] public string Uuid { get; set; } + /// + /// Gets Uuid for this login request + /// + /// + public string GetUuid() + { + return $"{Uuid}|{Email}"; + } + } } diff --git a/BlueWest.Api/Users/UserRepository.cs b/BlueWest.Api/Users/UserRepository.cs index 7710a12..ecdf383 100644 --- a/BlueWest.Api/Users/UserRepository.cs +++ b/BlueWest.Api/Users/UserRepository.cs @@ -1,28 +1,26 @@ -using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using BlueWest.WebApi.EF; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; -namespace BlueWest.WebApi.Context.Users; - -/// -/// Users Repository -/// -public class UserRepository : UserStore +namespace BlueWest.WebApi.Context.Users { - private readonly ApplicationUserDbContext _context; + /// + /// Users Repository + /// + public class UserRepository : UserStore + { + private readonly ApplicationUserDbContext _context; /// /// User repository @@ -66,4 +64,5 @@ public class UserRepository : UserStore