🔑 Login Stuff ya' know 🔑

This commit is contained in:
Stone_Red
2023-02-01 10:17:53 +01:00
parent 96a0d710c4
commit 5d6db0b5c1
9 changed files with 187 additions and 17 deletions
@@ -6,5 +6,9 @@ namespace Elektrifikatsiya.Database;
public class UserDatabaseContext : DbContext public class UserDatabaseContext : DbContext
{ {
public UserDatabaseContext(DbContextOptions<UserDatabaseContext> dbContextOptions) : base(dbContextOptions)
{
}
public DbSet<User> Users { get; set; } public DbSet<User> Users { get; set; }
} }
@@ -2,11 +2,16 @@ using Blazorise;
using Blazorise.Icons.Material; using Blazorise.Icons.Material;
using Blazorise.Material; using Blazorise.Material;
using Elektrifikatsiya.Database;
using Microsoft.EntityFrameworkCore;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args); WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Add services to the container. // Add services to the container.
builder.Services.AddRazorPages(); builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor(); builder.Services.AddServerSideBlazor();
builder.Services.AddDbContext<UserDatabaseContext>(options => options.UseSqlite("/host/UserDatabase.sqlite"));
AddBlazorise(builder.Services); AddBlazorise(builder.Services);
@@ -18,7 +18,7 @@ public interface IAuthenticationService
/// <param name="email">The E-mail address of the user.</param> /// <param name="email">The E-mail address of the user.</param>
/// <returns>A <see cref="Task"/> to <see langword="await"/>.</returns> /// <returns>A <see cref="Task"/> to <see langword="await"/>.</returns>
Task<Result> RegisterUserAsync(string name, string password); Task<Result> RegisterUserAsync(string name, string password, Role role);
/// <summary> /// <summary>
/// Logs a user in. /// Logs a user in.
@@ -38,7 +38,7 @@ public interface IAuthenticationService
/// </summary> /// </summary>
/// <param name="deletionToken">The deletion token.</param> /// <param name="deletionToken">The deletion token.</param>
/// <returns>A <see cref="Task"/> to <see langword="await"/> and a <see cref="bool"/> that indicates if the operation was successful.</returns> /// <returns>A <see cref="Task"/> to <see langword="await"/> and a <see cref="bool"/> that indicates if the operation was successful.</returns>
Task<Result> DeleteUserAsync(string deletionToken); Task<Result> DeleteUserAsync();
/// <summary> /// <summary>
/// Checks if a user exists. /// Checks if a user exists.
@@ -0,0 +1,13 @@
namespace Elektrifikatsiya.Services;
public interface ICookieService
{
/// <summary>
/// Writes a cookie.
/// </summary>
/// <param name="name">The name of the cookie.</param>
/// <param name="value">The value of the cookie.</param>
/// <param name="days">Indicates the maximum lifetime of the cookie.</param>
/// <returns></returns>
Task WriteCookieAsync(string name, string value, int days);
}
@@ -1,43 +1,134 @@
using Elektrifikatsiya.Models; using Elektrifikatsiya.Database;
using Elektrifikatsiya.Models;
using Elektrifikatsiya.Utilities;
using FluentResults; using FluentResults;
using Microsoft.EntityFrameworkCore;
using BC = BCrypt.Net.BCrypt;
namespace Elektrifikatsiya.Services.Implementations; namespace Elektrifikatsiya.Services.Implementations;
public class AuthenticationService : IAuthenticationService public class AuthenticationService : IAuthenticationService
{ {
public Task<Result> DeleteUserAsync(string deletionToken) private readonly UserDatabaseContext userDatabaseContext;
private readonly IHttpContextAccessor httpContextAccessor;
private readonly ICookieService cookieService;
public AuthenticationService(UserDatabaseContext userDatabaseContext, IHttpContextAccessor httpContextAccessor, ICookieService cookieService)
{ {
throw new NotImplementedException(); this.userDatabaseContext = userDatabaseContext;
this.httpContextAccessor = httpContextAccessor;
this.cookieService = cookieService;
} }
public Task<Result<User>> GetUserAsync() public async Task<Result> DeleteUserAsync()
{ {
throw new NotImplementedException(); Result<User> getUserResult = await GetUserAsync();
if (getUserResult.IsFailed)
{
return getUserResult.ToResult();
} }
public Task<Result<bool>> IsAuthenticated() _ = userDatabaseContext.Users.Remove(getUserResult.Value);
{ return (await Result.Try(() => userDatabaseContext.SaveChangesAsync())).ToResult();
throw new NotImplementedException();
} }
public Task<Result> LoginUserAsync(string name, string password) public async Task<Result<User>> GetUserAsync()
{ {
throw new NotImplementedException(); string? token = httpContextAccessor.HttpContext?.Request.Cookies["token"]?.ToString();
if (!TokenGenerator.ValidateToken(token, "auth"))
{
return Result.Fail("Token is not valid!");
} }
public Task<Result> LogoutUserAsync() User? user = await userDatabaseContext.Users.FirstOrDefaultAsync(u => u.SessionToken == token);
if (user is null || DateTime.UtcNow - user.LastLoginDate > TimeSpan.FromDays(7))
{ {
throw new NotImplementedException(); return Result.Fail("Token is not valid or expired!");
} }
public Task<Result> RegisterUserAsync(string name, string password) return user;
}
public async Task<Result<bool>> IsAuthenticated()
{ {
throw new NotImplementedException(); return (await GetUserAsync()).IsSuccess;
}
public async Task<Result> LoginUserAsync(string name, string password)
{
User? user = await userDatabaseContext.Users.FirstOrDefaultAsync(u => u.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
if (user is null)
{
return Result.Fail("User does not exist!");
}
if (!BC.Verify(password, user.PasswordHash))
{
return Result.Fail("Invalid credentials!");
}
string newToken = TokenGenerator.GenerateToken("auth", user.Id);
user.LastLoginDate = DateTime.UtcNow;
user.SessionToken = newToken;
_ = await userDatabaseContext.SaveChangesAsync();
await cookieService.WriteCookieAsync("token", newToken, 7);
return Result.Ok();
}
public async Task<Result> LogoutUserAsync()
{
string? token = httpContextAccessor.HttpContext?.Request.Cookies["token"]?.ToString();
await cookieService.WriteCookieAsync("token", string.Empty, 0);
if (!TokenGenerator.ValidateToken(token, "auth"))
{
return Result.Ok();
}
User? user = await userDatabaseContext.Users.FirstOrDefaultAsync(u => u.SessionToken == token);
if (user is null)
{
return Result.Ok();
}
user.SessionToken = null;
_ = await userDatabaseContext.SaveChangesAsync();
return Result.Ok();
}
public async Task<Result> RegisterUserAsync(string name, string password, Role role)
{
Result<bool> userExistsResult = await UserExistsAsync(name);
if (userExistsResult.IsFailed || userExistsResult.Value)
{
return Result.Fail("User name already exists!");
}
User user = new User(name, BC.HashPassword(password), role);
_ = userDatabaseContext.Users.Add(user);
return (await Result.Try(() => userDatabaseContext.SaveChangesAsync())).ToResult();
} }
public Task<Result<bool>> UserExistsAsync(string name) public Task<Result<bool>> UserExistsAsync(string name)
{ {
throw new NotImplementedException(); return Result.Try(() =>
userDatabaseContext.Users.AnyAsync(u => u.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)));
} }
} }
@@ -23,7 +23,7 @@ public class AuthorizationService : IAuthorizationService
} }
else else
{ {
return Result.Fail("Use doesn't have the required role!"); return Result.Fail("User doesn't have the required role!");
} }
} }
} }
@@ -0,0 +1,18 @@
using Microsoft.JSInterop;
namespace Elektrifikatsiya.Services.Implementations;
public class CookieService : ICookieService
{
private readonly IJSRuntime jsRuntime;
public CookieService(IJSRuntime jsRuntime)
{
this.jsRuntime = jsRuntime;
}
public async Task WriteCookieAsync(string name, string value, int days)
{
_ = await jsRuntime.InvokeAsync<string>("blazorExtensions.WriteCookie", name, value, days);
}
}
@@ -0,0 +1,20 @@
using System.Security.Cryptography;
namespace Elektrifikatsiya.Utilities;
public static class SecureStringGenerator
{
public static string CreateCryptographicRandomString(int count)
{
return Convert.ToBase64String(RandomNumberGenerator.GetBytes(count));
}
public static string CreateCryptographicRandomString(int count, int uid)
{
List<byte> bytes = RandomNumberGenerator.GetBytes(count).ToList();
bytes.AddRange(BitConverter.GetBytes(uid));
return Convert.ToBase64String(bytes.ToArray());
}
}
@@ -0,0 +1,19 @@
namespace Elektrifikatsiya.Utilities;
public static class TokenGenerator
{
public static string GenerateToken(string tokenType, int uid = 0, int length = 128)
{
return tokenType + "-" + SecureStringGenerator.CreateCryptographicRandomString(length, uid);
}
public static bool ValidateToken(string? token, string tokenType)
{
if (token is null)
{
return false;
}
return token.StartsWith(tokenType + "-");
}
}