Redis working

This commit is contained in:
Wvader 2022-09-18 02:00:24 +01:00
parent 3b8d82049f
commit b697a4b357
27 changed files with 452 additions and 394 deletions

View File

@ -87,11 +87,6 @@ namespace BlueWest.WebApi.EF.Model
builder.Entity<ApplicationRoleClaim>().ToTable("RoleClaims"); builder.Entity<ApplicationRoleClaim>().ToTable("RoleClaims");
builder.Entity<ApplicationUserRole>().ToTable("UserRole"); builder.Entity<ApplicationUserRole>().ToTable("UserRole");
// Session Token
builder.Entity<SessionToken>()
.HasOne(b => b.User)
.WithMany(x => x.SessionToken)
.HasForeignKey(x => x.UserId);
// Session Token Primary Key // Session Token Primary Key
builder.Entity<SessionToken>(b => builder.Entity<SessionToken>(b =>
@ -105,12 +100,6 @@ namespace BlueWest.WebApi.EF.Model
.WithMany(x => x.SessionDatas) .WithMany(x => x.SessionDatas)
.HasForeignKey(x => x.UserId); .HasForeignKey(x => x.UserId);
// Session Data
builder.Entity<SessionData>()
.HasOne(b => b.SessionToken)
.WithOne(x => x.SessionData);
// Session Data Primary Key // Session Data Primary Key
builder.Entity<SessionData>(b => builder.Entity<SessionData>(b =>

View File

@ -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
{
/// <summary>
/// CountryDbContext Constructor.
/// </summary>
/// <param name="options"></param>
public SessionDbContext(DbContextOptions<SessionDbContext> options) : base(options)
{
Database.EnsureCreated();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ConfigureCurrentDbModel();
}
public DbSet<SessionToken> SessionTokens { get; set; }
}
}

View File

@ -1,14 +1,10 @@
using System; using System;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using BlueWest.Cryptography;
using BlueWest.Data.Application;
using BlueWest.WebApi.Context.Users; using BlueWest.WebApi.Context.Users;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -19,29 +15,24 @@ namespace BlueWest.WebApi.Controllers;
/// </summary> /// </summary>
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
[Authorize(Policy = SessionConstants.ApiNamePolicy)]
[Authorize(Policy = "ApiUser")]
/*[EnableCors(Constants.CorsPolicyName)]*/ /*[EnableCors(Constants.CorsPolicyName)]*/
public class AuthController : Controller public class AuthController : Controller
{ {
private readonly IAuthManager _authManager; private readonly IAuthManager _authManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly ISessionManager _sessionManager;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
/// <param name="authManager"></param> /// <param name="authManager"></param>
/// <param name="userManager"></param> /// <param name="userManager"></param>
public AuthController( IAuthManager authManager, IUserManager userManager, ISessionManager sessionManager) public AuthController( IAuthManager authManager, IUserManager userManager)
{ {
_authManager = authManager; _authManager = authManager;
_userManager = userManager; _userManager = userManager;
_sessionManager = sessionManager;
} }
/// <summary> /// <summary>
/// Signup user /// Signup user
/// </summary> /// </summary>
@ -62,64 +53,49 @@ namespace BlueWest.WebApi.Controllers;
/// <param name="loginViewModel"></param> /// <param name="loginViewModel"></param>
/// <returns></returns> /// <returns></returns>
[AllowAnonymous] [AllowAnonymous]
[HttpPost("token")] [HttpPost("login")]
public async Task<ActionResult<IdentityResult>> GetTokenAsync(LoginRequest loginViewModel) public async Task<ActionResult<IdentityResult>> GetSessionToken(LoginRequest loginViewModel)
{ {
var (success, sessionToken, token) = await _authManager.GetToken(loginViewModel); var (success, sessionToken, identity) = await _authManager.GetSessionTokenId(loginViewModel);
if (success) if (success)
{ {
return Ok(new {sessionToken, token}); return Ok(new {sessionToken});
} }
return Problem(); return Problem();
} }
/// <summary> /// <summary>
/// Check if user is logged in /// Gets a bearer token
/// </summary> /// </summary>
/// <param name="loginViewModel"></param>
/// <returns></returns> /// <returns></returns>
[HttpGet("isLoggedIn")] [AllowAnonymous]
[HttpPost("bearer")]
public ActionResult<bool> IsLoggedIn() public async Task<ActionResult<IdentityResult>> GetBearerBySessionId(string sessionId)
{ {
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme); var (success, bearer) = await _authManager.GetBearerTokenBySessionTokenId(sessionId);
if (identity.IsAuthenticated) if (success)
{ {
return Ok(true); return Ok(new {bearer});
} }
return new UnauthorizedObjectResult(new {message = "The provided sessionId didn't return a valid Token."});
return Ok(false);
} }
/// <summary>
/// Checks if the session is authorized
/// </summary>
/// <param name="hash"></param>
/// <returns></returns>
[HttpGet("isAuthorized")]
public ActionResult IsAuthorized(string hash)
{
var isAuthorized = _sessionManager.IsAuthorized(hash);
return Ok(isAuthorized ? new {authenticated = true} : new {authenticated = false});
}
/// <summary> /// <summary>
/// Do Cookie based login. /// Do Cookie based login.
/// </summary> /// </summary>
/// <param name="loginDto"></param> /// <param name="loginDto"></param>
/// <returns></returns> /// <returns></returns>
[AllowAnonymous] /*[AllowAnonymous]
[HttpPost("login")] [HttpPost("login")]
public async Task<ActionResult> DoLoginAsync(LoginRequest loginDto) public async Task<ActionResult> DoLoginByCookie(LoginRequest loginDto)
{ {
var (success, identity, sessionToken) = await _authManager.DoLogin(loginDto); var (success, sessionToken, identity) = await _authManager.GetSessionTokenId(loginDto);
if (success) if (success)
{ {
@ -129,23 +105,22 @@ namespace BlueWest.WebApi.Controllers;
new AuthenticationProperties new AuthenticationProperties
{ {
IsPersistent = true, IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddDays(1) ExpiresUtc = DateTime.UtcNow.Add(SessionConstants.DefaultValidForSpan)
}); });
return Ok(new {authenticated = true, sessionToken}); return Ok(new {authenticated = true, sessionToken});
} }
return new ForbidResult(CookieAuthenticationDefaults.AuthenticationScheme); return new ForbidResult(CookieAuthenticationDefaults.AuthenticationScheme);
} }*/
/// <summary> /// <summary>
/// Do Cookie based logout /// Do Cookie based logout
/// </summary> /// </summary>
/// <param name="loginDto"></param>
/// <returns></returns> /// <returns></returns>
[AllowAnonymous] [AllowAnonymous]
[HttpPost("logout")] [HttpPost("logout")]
public async Task DoLogoutAsync() public async Task DoCookieLogoutAsync()
{ {
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
} }

View File

@ -18,7 +18,7 @@ namespace BlueWest.WebApi.Controllers
/// </summary> /// </summary>
[ApiController] [ApiController]
[Route("[controller]")] [Route("[controller]")]
[Authorize(Policy = "ApiUser")] [Authorize(Policy = SessionConstants.ApiNamePolicy)]
[EnableCors(Constants.CorsPolicyName)] [EnableCors(Constants.CorsPolicyName)]
// [Authorize(Roles = "Administrator")] // [Authorize(Roles = "Administrator")]
public class CountryController : ControllerBase public class CountryController : ControllerBase

View File

@ -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;
/// <summary>
/// Index Creation Device
/// </summary>
/// <param name="provider"></param>
public IndexCreationDevice(RedisConnectionProvider provider)
{
_provider = provider;
}
/// <inheritdoc />
public async Task StartAsync(CancellationToken cancellationToken)
{
await _provider.Connection.CreateIndexAsync(typeof(SessionToken)); }
/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask; }
}
}

View File

@ -0,0 +1,48 @@
using System.Threading.Tasks;
using BlueWest.Data.Application;
using BlueWest.WebApi.Context.Users;
using Microsoft.Extensions.Hosting;
namespace BlueWest.WebApi
{
/// <summary>
/// Methods for handling session cache data.
/// </summary>
public interface ISessionCache : IHostedService
{
/// <summary>
/// Gets a Bearer By Access Token Id
/// </summary>
/// <param name="sessionTokenId"></param>
/// <returns></returns>
Task<string> GetBearerByAccessTokenId(string sessionTokenId);
/// <summary>
/// Gets a Session Token by Id.
/// </summary>
/// <param name="tokenId"></param>
/// <returns></returns>
Task<SessionToken> GetSessionTokenByIdAsync(string tokenId);
/// <summary>
/// Create a new session token
/// </summary>
/// <param name="token"></param>
Task AddSessionToken(SessionToken token);
/// <summary>
/// Save Cache
/// </summary>
/// <returns></returns>
Task SaveAsync();
/// <summary>
/// Save Cache
/// </summary>
/// <returns></returns>
void Save();
}
}

View File

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

View File

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

View File

@ -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
{
/// <summary>
/// Session Provider Context
/// </summary>
public sealed class SessionDataService : IHostedService, ISessionCache
{
private readonly RedisConnectionProvider _provider;
private RedisCollection<SessionToken> _sessionTokens;
/// <summary>
/// Index Creation Device
/// </summary>
/// <param name="provider">Redis connection</param>
public SessionDataService(
RedisConnectionProvider provider)
{
_provider = provider;
_sessionTokens = (RedisCollection<SessionToken>)provider.RedisCollection<SessionToken>();
}
/// <summary>
/// Empty constructor
/// </summary>
public SessionDataService() { }
/// <summary>
/// Get a session token by the respective Id.
/// </summary>
/// <param name="tokenId"></param>
/// <returns></returns>
public async Task<SessionToken> GetSessionTokenByIdAsync(string tokenId)
{
return await _sessionTokens.Where(x => x.Id == tokenId)
.FirstOrDefaultAsync();
}
/// <summary>
/// Create a new session token
/// </summary>
/// <param name="token"></param>
public async Task AddSessionToken(SessionToken token)
{
await _sessionTokens.InsertAsync(token);
}
/// <inheritdoc />
public async Task SaveAsync()
{
await _sessionTokens.SaveAsync();
}
/// <summary>
/// Save session data
/// </summary>
public void Save()
{
_sessionTokens.Save();
}
/// <summary>
/// Gets a Bearer By Access Token Id
/// </summary>
/// <param name="sessionTokenId"></param>
public async Task<string> 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;
}
/// <inheritdoc />
public async Task StartAsync(CancellationToken cancellationToken)
{
await _provider.Connection.CreateIndexAsync(typeof(SessionToken));
}
/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}

View File

@ -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;
}
/// <summary>
/// Check if token is authorized
/// </summary>
/// <param name="hash"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Gets a new or existing session token
/// </summary>
/// <param name="hash"></param>
/// <param name="applicationUser"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Gets a session token for the user following a login request
/// </summary>
/// <param name="loginRequest"></param>
/// <param name="user"></param>
/// <returns></returns>
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;
}
}
}

View File

@ -153,7 +153,7 @@ namespace BlueWest.WebApi
switch (allowedDatabase) switch (allowedDatabase)
{ {
case "mysql": case "mysql":
services.PrepareMySqlDatabasePool(_configuration, _environment); services.PrepareMySqlDatabasePool(_configuration, _environment, configuration);
break; break;
case "sqlite": case "sqlite":

View File

@ -5,21 +5,19 @@ using BlueWest.Cryptography;
using BlueWest.WebApi.Context; using BlueWest.WebApi.Context;
using BlueWest.WebApi.Context.Users; using BlueWest.WebApi.Context.Users;
using BlueWest.WebApi.EF; using BlueWest.WebApi.EF;
using Microsoft.AspNetCore.Authentication; using BlueWest.WebApi.Session;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models; using Redis.OM;
namespace BlueWest.WebApi namespace BlueWest.WebApi
{ {
@ -39,13 +37,36 @@ namespace BlueWest.WebApi
private static DbContextOptionsBuilder GetMySqlSettings( private static DbContextOptionsBuilder GetMySqlSettings(
this DbContextOptionsBuilder optionsBuilder, this DbContextOptionsBuilder optionsBuilder,
IConfiguration configuration, IConfiguration configuration,
IConfigurationRoot configurationRoot,
IWebHostEnvironment environment) IWebHostEnvironment environment)
{ {
var sqlVersion = GetMySqlServerVersion(8, 0, 11); var sqlVersion = GetMySqlServerVersion(8, 0, 11);
// Docker / No-Docker
var startupMode = configurationRoot["mode"];
string mySqlConnectionString = String.Empty;
if (startupMode == "docker")
{
var config = configuration.Get<ConnectionStringDocker>();
if(config != null) mySqlConnectionString = config.MySql;
}
else
{
var config = configuration.Get<ConnectionStringNoDocker>();
if(config != null) mySqlConnectionString = config.MySql;
}
if (mySqlConnectionString == string.Empty)
{
throw new InvalidOperationException("Fatal error: MySQL Connection string is empty.");
}
optionsBuilder optionsBuilder
.UseMySql( .UseMySql(
configuration.GetConnectionString("DockerMySQL"), mySqlConnectionString,
sqlVersion) sqlVersion)
.UseMySql(sqlVersion, .UseMySql(sqlVersion,
builder => builder =>
@ -74,16 +95,19 @@ namespace BlueWest.WebApi
/// <param name="environment"></param> /// <param name="environment"></param>
/// <returns></returns> /// <returns></returns>
public static IServiceCollection PrepareMySqlDatabasePool(this IServiceCollection serviceCollection, public static IServiceCollection PrepareMySqlDatabasePool(this IServiceCollection serviceCollection,
IConfiguration configuration, IWebHostEnvironment environment) IConfiguration configuration, IWebHostEnvironment environment, IConfigurationRoot configurationRoot)
{ {
return serviceCollection return serviceCollection
.AddDbContextPool<UserDbContext>(options => options.GetMySqlSettings(configuration, environment)) .AddDbContextPool<UserDbContext>(options =>
.AddDbContextPool<CountryDbContext>(options => options.GetMySqlSettings(configuration, environment)) options.GetMySqlSettings(configuration, configurationRoot, environment))
.AddDbContextPool<FinanceDbContext>(options => options.GetMySqlSettings(configuration, environment)) .AddDbContextPool<CountryDbContext>(options =>
.AddDbContextPool<CompanyDbContext>(options => options.GetMySqlSettings(configuration, environment)) options.GetMySqlSettings(configuration, configurationRoot, environment))
.AddDbContextPool<FinanceDbContext>(options =>
options.GetMySqlSettings(configuration, configurationRoot, environment))
.AddDbContextPool<CompanyDbContext>(options =>
options.GetMySqlSettings(configuration, configurationRoot, environment))
.AddDbContextPool<ApplicationUserDbContext>(options => .AddDbContextPool<ApplicationUserDbContext>(options =>
options.GetMySqlSettings(configuration, environment)) options.GetMySqlSettings(configuration, configurationRoot, environment));
.AddDbContextPool<SessionDbContext>(options => options.UseInMemoryDatabase("_s"));
} }
/// <summary> /// <summary>
@ -103,22 +127,23 @@ namespace BlueWest.WebApi
.AddDbContextPool<CountryDbContext>(options => options.UseSqlite(sqliteConString)) .AddDbContextPool<CountryDbContext>(options => options.UseSqlite(sqliteConString))
.AddDbContextPool<FinanceDbContext>(options => options.UseSqlite(sqliteConString)) .AddDbContextPool<FinanceDbContext>(options => options.UseSqlite(sqliteConString))
.AddDbContextPool<CompanyDbContext>(options => options.UseSqlite(sqliteConString)) .AddDbContextPool<CompanyDbContext>(options => options.UseSqlite(sqliteConString))
.AddDbContextPool<ApplicationUserDbContext>(options => options.UseSqlite(sqliteConString)) .AddDbContextPool<ApplicationUserDbContext>(options => options.UseSqlite(sqliteConString));
.AddDbContextPool<SessionDbContext>(options => options.UseInMemoryDatabase("_s"));
} }
internal static IServiceCollection AddAuthServerServices(this IServiceCollection services, IConfiguration configuration , IWebHostEnvironment environment) internal static IServiceCollection AddAuthServerServices(this IServiceCollection services, IConfiguration configuration , IWebHostEnvironment environment)
{ {
services.AddScoped<IJwtTokenHandler, JwtTokenHandler>()
services
.AddSingleton(new RedisConnectionProvider("redis://redisinstance:6379"))
.AddScoped<IJwtTokenHandler, JwtTokenHandler>()
.AddScoped<IJwtFactory, JwtFactory>() .AddScoped<IJwtFactory, JwtFactory>()
.AddSingleton<IndexCreationDevice>() .AddHostedService<SessionDataService>()
.AddScoped<ISessionManager, SessionManager>() .AddSingleton<ISessionCache, SessionDataService>()
.AddScoped<UserRepository>() .AddScoped<UserRepository>()
.AddScoped<IUserManager, ApplicationUserManager>() .AddScoped<IUserManager, ApplicationUserManager>()
.AddScoped<IAuthManager, AuthManager>() .AddScoped<IAuthManager, AuthManager>()
.AddScoped<IHasher, Hasher>(); .AddScoped<IHasher, Hasher>();
// Database Context and Swagger // Database Context and Swagger
@ -201,7 +226,7 @@ namespace BlueWest.WebApi
// api user claim policy // api user claim policy
services.AddAuthorization(options => services.AddAuthorization(options =>
{ {
options.AddPolicy("ApiUser", options.AddPolicy(SessionConstants.ApiNamePolicy,
policy => policy.RequireClaim(Context.Users.Constants.JwtClaimIdentifiers.Rol, policy => policy.RequireClaim(Context.Users.Constants.JwtClaimIdentifiers.Rol,
Context.Users.Constants.JwtClaims.ApiAccess)); Context.Users.Constants.JwtClaims.ApiAccess));
@ -230,6 +255,15 @@ namespace BlueWest.WebApi
return services; return services;
} }
} }
} }
internal class BlueWestConnectionString
{
public string Redis { get; set; }
public string MySql { get; set; }
}
internal class ConnectionStringDocker : BlueWestConnectionString { }
internal class ConnectionStringNoDocker : BlueWestConnectionString { }

View File

@ -1,92 +1,130 @@
using System; using System;
using System.Security.Claims; using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BlueWest.Cryptography; using BlueWest.Cryptography;
using BlueWest.Data.Application; using BlueWest.Data.Application;
using Duende.IdentityServer.Extensions;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
namespace BlueWest.WebApi.Context.Users; namespace BlueWest.WebApi.Context.Users
internal class AuthManager : IAuthManager
{ {
internal class AuthManager : IAuthManager
{
private readonly ApplicationUserManager _userManager; private readonly ApplicationUserManager _userManager;
private readonly UserRepository _usersRepo;
private readonly ISessionManager _sessionManager;
private readonly IHasher _hasher; private readonly IHasher _hasher;
private readonly IJwtFactory _jwtFactory; private readonly IJwtFactory _jwtFactory;
private readonly ISessionCache _sessionCache;
/// <summary> /// <summary>
/// Auth manager constructor /// Auth manager constructor
/// </summary> /// </summary>
/// <param name="userManager"></param> /// <param name="userManager"></param>
/// <param name="hasher"></param> /// <param name="hasher"></param>
/// <param name="usersRepo"></param>
/// <param name="jwtFactory"></param> /// <param name="jwtFactory"></param>
/// <param name="sessionCache"></param>
public AuthManager( public AuthManager(
ApplicationUserManager userManager, ApplicationUserManager userManager,
IHasher hasher, IHasher hasher,
UserRepository usersRepo, IJwtFactory jwtFactory,
ISessionManager sessionManager, ISessionCache sessionCache)
IJwtFactory jwtFactory)
{ {
_userManager = userManager; _userManager = userManager;
_hasher = hasher; _hasher = hasher;
_usersRepo = usersRepo;
_jwtFactory = jwtFactory; _jwtFactory = jwtFactory;
_sessionManager = sessionManager; _sessionCache = sessionCache;
} }
public async Task<(bool, ClaimsIdentity, SessionTokenUnique)> DoLogin(LoginRequest loginRequest) private async Task<SessionToken> 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); 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)) if (!await _userManager.CheckPasswordAsync(user, loginRequest.Password)) return (false, string.Empty, null);
{
// Identity
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme); var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(ClaimTypes.Email, user.Email)); identity.AddClaim(new Claim(ClaimTypes.Email, user.Email));
// Session // Session
var sessionToken = _sessionManager.GetSessionToken(loginRequest, user); var sessionToken = await GetSessionToken(loginRequest);
var sessionResponse = new SessionTokenUnique(sessionToken);
return (true, identity, sessionResponse);
}
}
return (false, null, null); if (sessionToken != null)
}
/// <inheritdoc />
public async Task<(bool, SessionTokenUnique, AccessToken)> GetToken(LoginRequest loginRequest)
{ {
if (!string.IsNullOrEmpty(loginRequest.Email) && !string.IsNullOrEmpty(loginRequest.Password)) if (SessionTokenIsValid(sessionToken))
{ {
var user = await _userManager.FindByEmailAsync(loginRequest.Email); return (true, sessionToken.Id, identity);
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 (success, bearerToken) = await GenerateBearerToken(identity, user);
var newSessionToken = GetNewSessionToken(loginRequest, user, bearerToken);
await _sessionCache.AddSessionToken(newSessionToken);
return (success, newSessionToken.Id, identity);
} }
private async Task<(bool, string)> GenerateBearerToken(ClaimsIdentity identity, ApplicationUser user)
{
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);
}
/// <inheritdoc /> /// <inheritdoc />
public async Task<bool> VerifyLoginByEmailAsync(string email, string password) public async Task<bool> VerifyLoginByEmailAsync(string email, string password)
{ {
@ -113,5 +151,5 @@ internal class AuthManager : IAuthManager
RegisterViewModel userToCreate = FromSignupToUser(userSignupDto); RegisterViewModel userToCreate = FromSignupToUser(userSignupDto);
return await _userManager.CreateAsync(userToCreate.ToUser()); return await _userManager.CreateAsync(userToCreate.ToUser());
} }
}
} }

View File

@ -80,18 +80,19 @@ namespace BlueWest.Cryptography
keyIndex = null; keyIndex = null;
trimmedCipherText = cipherText; 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; algorithm = foundAlgorithm;
} }
if (cipherInfo.Length == 2 && int.TryParse(cipherInfo[1], out var foundKeyIndex)) if (int.TryParse(cipherInfo, out int foundKeyIndex))
keyIndex = foundKeyIndex; keyIndex = foundKeyIndex;
trimmedCipherText = cipherText.Substring(cipherText.IndexOf(']') + 1);
trimmedCipherText = cipherText.Substring(cipherText.IndexOf(cipherInfo, StringComparison.Ordinal) + 1);
} }
} }
} }

View File

@ -39,7 +39,7 @@ internal class JwtIssuerOptions
/// <summary> /// <summary>
/// Set the timespan the token will be valid for (default is 120 min) /// Set the timespan the token will be valid for (default is 120 min)
/// </summary> /// </summary>
public TimeSpan ValidFor { get; set; } = TimeSpan.FromMinutes(120); public TimeSpan ValidFor { get; set; } = SessionConstants.DefaultValidForSpan;
/// <summary> /// <summary>
/// "jti" (JWT ID) Claim (default ID is a GUID) /// "jti" (JWT ID) Claim (default ID is a GUID)

View File

@ -26,11 +26,11 @@ namespace BlueWest.Cryptography
if (storeSalt) if (storeSalt)
{ {
hash = $"[{(int)HashAlgorithm.SHA3_512}]{salt}{asString}"; hash = $"{(int)HashAlgorithm.SHA3_512}{salt}{asString}";
return hash; return hash;
} }
hash = $"[{(int)HashAlgorithm.SHA3_512}]{asString}"; hash = $"{(int)HashAlgorithm.SHA3_512}{asString}";
return hash; return hash;
} }

View File

@ -18,26 +18,18 @@ public interface IAuthManager
/// <returns></returns> /// <returns></returns>
Task<IdentityResult> CreateUserAsync(RegisterViewModel registerViewModel); Task<IdentityResult> CreateUserAsync(RegisterViewModel registerViewModel);
/// <summary>
/// VerifyLoginAsync
/// </summary>
/// <param name="email"></param>
/// <param name="password"></param>
/// <returns></returns>
Task<bool> VerifyLoginByEmailAsync(string email, string password);
/// <summary>
/// GetToken
/// </summary>
/// <param name="loginRequest"></param>
/// <returns></returns>
Task<(bool, SessionTokenUnique, AccessToken)> GetToken(LoginRequest loginRequest);
/// <summary> /// <summary>
/// Does Login /// Does Login
/// </summary> /// </summary>
/// <param name="loginRequest"></param> /// <param name="loginRequest"></param>
/// <returns></returns> /// <returns></returns>
Task<(bool, ClaimsIdentity, SessionTokenUnique)> DoLogin(LoginRequest loginRequest); public Task<(bool, string, ClaimsIdentity)> GetSessionTokenId(LoginRequest loginRequest);
/// <summary>
/// Gets a valid bearer token by the session id
/// </summary>
/// <param name="sessionId"></param>
/// <returns></returns>
Task<(bool, string)> GetBearerTokenBySessionTokenId(string sessionId);
} }

View File

@ -25,6 +25,15 @@ namespace BlueWest.WebApi.Context.Users
[Required] [Required]
public string Uuid { get; set; } public string Uuid { get; set; }
/// <summary>
/// Gets Uuid for this login request
/// </summary>
/// <returns></returns>
public string GetUuid()
{
return $"{Uuid}|{Email}";
}
} }
} }

View File

@ -1,18 +1,16 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BlueWest.WebApi.EF;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace BlueWest.WebApi.Context.Users; namespace BlueWest.WebApi.Context.Users
{
/// <summary> /// <summary>
/// Users Repository /// Users Repository
/// </summary> /// </summary>
public class UserRepository : UserStore<ApplicationUser, public class UserRepository : UserStore<ApplicationUser,
ApplicationRole, ApplicationRole,
ApplicationUserDbContext, ApplicationUserDbContext,
string, string,
@ -21,7 +19,7 @@ public class UserRepository : UserStore<ApplicationUser,
ApplicationUserLogin, ApplicationUserLogin,
ApplicationUserToken, ApplicationUserToken,
ApplicationRoleClaim> ApplicationRoleClaim>
{ {
private readonly ApplicationUserDbContext _context; private readonly ApplicationUserDbContext _context;
/// <summary> /// <summary>
@ -66,4 +64,5 @@ public class UserRepository : UserStore<ApplicationUser,
return Task.FromResult(!string.IsNullOrEmpty(user.PasswordHash)); return Task.FromResult(!string.IsNullOrEmpty(user.PasswordHash));
} }
}
} }

View File

@ -7,10 +7,15 @@
} }
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"ConnectionStrings": { "ConnectionStringDocker": {
"DockerMySQL": "server=db;user=blueuser;password=dXjw127124dJ;database=bluedb;" "MySQL": "server=db;user=blueuser;password=dXjw127124dJ;database=bluedb;",
"Redis": "redis://redisinstance:6379"
}, },
"REDIS_CONNECTION_STRING": "redis://localhost:6379", "ConnectionStringNoDocker": {
"MySQL": "server=localhost;user=blueuser;password=dXjw127124dJ;database=bluedb;",
"Redis": "redis://localhost:6379"
},
"REDIS_CONNECTION_STRING": "redis://redis:6379",
"AuthSettings": { "AuthSettings": {
"SecretKey": "iJWHDmHLpUA283sqsfhqGbMRdRj1PVkH" "SecretKey": "iJWHDmHLpUA283sqsfhqGbMRdRj1PVkH"
}, },

View File

@ -1,4 +1,5 @@
{ {
"mode": "docker",
"database": "sqlite", "database": "sqlite",
"environment": "dev" "environment": "dev"
} }

View File

@ -1,8 +1,10 @@
using BlueWest.WebApi.Context.Users; using BlueWest.WebApi.Context.Users;
using MapTo; using MapTo;
using Redis.OM.Modeling;
namespace BlueWest.Data.Application namespace BlueWest.Data.Application
{ {
[Document(StorageType = StorageType.Json, Prefixes = new []{"SessionToken"})]
[MapFrom(new [] [MapFrom(new []
{ {
typeof(SessionTokenUnique) typeof(SessionTokenUnique)
@ -10,23 +12,18 @@ namespace BlueWest.Data.Application
public partial class SessionToken public partial class SessionToken
{ {
[IgnoreMemberMapTo] [IgnoreMemberMapTo]
public string Id { get; set; } [Indexed] public string Id { get; set; }
public string Token { get; set; } [Indexed] public int ValidFor { get; set;}
public TimeSpan ValidFor { get; set;}
public bool IsValid { get; set; } [Indexed] public bool IsValid { get; set; }
public DateTime CreatedDate { get; set; } [Indexed] public long CreatedDate { get; set; }
public ApplicationUser User { get; set; } [Indexed] public string UserId { get; set; }
public string UserId { get; set; }
public ApplicationDevice ApplicationDevice { get; set; } [Indexed] public string AccessToken { get; set; }
public SessionData SessionData { get; set; }
public string AccessToken { get; set; }
} }
} }

View File

@ -7,9 +7,8 @@ namespace BlueWest.Data.Application
{ {
public string Id { get; set; } public string Id { get; set; }
public string Token { get; set; } public int ValidFor { get; set;}
public TimeSpan ValidFor { get; set;} public long CreatedDate { get; set; }
public DateTime CreatedDate { get; set; }
public string UserId { get; set; } public string UserId { get; set; }
} }

View File

@ -23,6 +23,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{D7BF4A
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
docker-compose.yml = docker-compose.yml docker-compose.yml = docker-compose.yml
BlueWest.Api\Dockerfile = BlueWest.Api\Dockerfile BlueWest.Api\Dockerfile = BlueWest.Api\Dockerfile
docker-compose.db.only.yml = docker-compose.db.only.yml
EndProjectSection EndProjectSection
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "README", "README", "{E9E3CEB0-D00C-46E3-B497-B4ED7B291190}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "README", "README", "{E9E3CEB0-D00C-46E3-B497-B4ED7B291190}"

0
data/appendonly.aof Normal file
View File

View File

@ -0,0 +1,29 @@
version: '3'
services:
db:
container_name: BW1_DB_MYSQL
image: mysql/mysql-server:8.0
environment:
MYSQL_ROOT_HOST: db
MYSQL_USER_HOST: db
MYSQL_ROOT_PASSWORD: dXjw127124dJ
MYSQL_USER: blueuser
MYSQL_PASSWORD: dXjw127124dJ
MYSQL_DATABASE: bluedb
volumes:
- ./docker-entrypoint-initdb.d/:/docker-entrypoint-initdb.d/
phpmyadmin:
container_name: BW_PHPMYADMIN
image: phpmyadmin/phpmyadmin
ports:
- 80:80
environment:
MYSQL_USERNAME: 'blueuser'
MYSQL_ROOT_PASSWORD: 'dXjw127124dJ'
# ports:
# - "3308:3306"
redis:
image: "redis:alpine"
command: redis-server
ports:
- "6379:6379"

View File

@ -1,7 +1,7 @@
version: '3' version: '3'
services: services:
db: db:
container_name: BW1_DB_MYSQL container_name: BW1_MYSQL
image: mysql/mysql-server:8.0 image: mysql/mysql-server:8.0
environment: environment:
MYSQL_ROOT_HOST: db MYSQL_ROOT_HOST: db
@ -12,16 +12,21 @@ services:
MYSQL_DATABASE: bluedb MYSQL_DATABASE: bluedb
volumes: volumes:
- ./docker-entrypoint-initdb.d/:/docker-entrypoint-initdb.d/ - ./docker-entrypoint-initdb.d/:/docker-entrypoint-initdb.d/
phpmyadmin: # phpmyadmin:
container_name: BW_PHPMYADMIN # container_name: BW_PHPMYADMIN
image: phpmyadmin/phpmyadmin # image: phpmyadmin/phpmyadmin
ports: # ports:
- 80:80 # - 80:80
environment: # environment:
MYSQL_USERNAME: 'blueuser' # MYSQL_USERNAME: 'blueuser'
MYSQL_ROOT_PASSWORD: 'dXjw127124dJ' # MYSQL_ROOT_PASSWORD: 'dXjw127124dJ'
# ports: # ports:
# - "3308:3306" # - "3308:3306"
redisinstance:
image: "redislabs/redismod"
ports:
- "6379:6379"
container_name: BW1_REDIS
bapi120: bapi120:
build: build:
context: ./ context: ./
@ -33,13 +38,5 @@ services:
restart: always restart: always
links: links:
- db:db - db:db
- redisinstance:redisinstance
container_name: BW1_API container_name: BW1_API
redis:
image: "redis:alpine"
command: redis-server --requirepass Sup3rSecurePass0rd
ports:
- "6379:6379"
environment:
- REDIS_REPLICATION_MODE=master