Files
Moonlight/Moonlight/Core/Services/IdentityService.cs

189 lines
6.0 KiB
C#

using Microsoft.EntityFrameworkCore;
using MoonCore.Abstractions;
using MoonCore.Attributes;
using MoonCore.Exceptions;
using MoonCore.Helpers;
using Moonlight.Core.Database.Entities;
using Moonlight.Core.Models.Abstractions;
using Moonlight.Core.Models.Enums;
using Moonlight.Core.Services.Utils;
using Moonlight.Features.StoreSystem.Entities;
using OtpNet;
namespace Moonlight.Core.Services;
// This service allows you to reauthenticate, login and force login
// It does also contain the permission system accessor for the current user
[Scoped]
public class IdentityService
{
private readonly Repository<User> UserRepository;
private readonly JwtService JwtService;
private string Token;
public User? CurrentUserNullable { get; private set; }
public User CurrentUser => CurrentUserNullable!;
public bool IsSignedIn => CurrentUserNullable != null;
public FlagStorage Flags { get; private set; } = new("");
public PermissionStorage Permissions { get; private set; } = new(-1);
public Transaction[] Transactions => GetTransactions().Result; // TODO: make more efficient
public EventHandler OnAuthenticationStateChanged { get; set; }
public IdentityService(Repository<User> userRepository,
JwtService jwtService)
{
UserRepository = userRepository;
JwtService = jwtService;
}
// Transactions
public Task<Transaction[]> GetTransactions()
{
if (CurrentUserNullable == null)
return Task.FromResult(Array.Empty<Transaction>());
var user = UserRepository
.Get()
.Include(x => x.Transactions)
.First(x => x.Id == CurrentUserNullable.Id);
return Task.FromResult(user.Transactions.ToArray());
}
// Authentication
public async Task Authenticate() // Reauthenticate
{
// Save the last id (or -1 if not set) so we can track a change
var lastUserId = CurrentUserNullable == null ? -1 : CurrentUserNullable.Id;
// Reset
CurrentUserNullable = null;
await ValidateToken();
// Get current user id to compare against the last one
var currentUserId = CurrentUserNullable == null ? -1 : CurrentUserNullable.Id;
if (lastUserId != currentUserId) // State changed, lets notify all event listeners
OnAuthenticationStateChanged?.Invoke(this, null!);
}
private async Task ValidateToken() // Read and validate token
{
if (string.IsNullOrEmpty(Token))
return;
if (!await JwtService.Validate(Token, "User"))
return;
var data = await JwtService.Decode(Token);
if (!data.ContainsKey("userId"))
return;
var userId = int.Parse(data["userId"]);
var user = UserRepository
.Get()
.FirstOrDefault(x => x.Id == userId);
if (user == null)
return;
if (!data.ContainsKey("issuedAt"))
return;
var issuedAt = long.Parse(data["issuedAt"]);
var issuedAtDateTime = DateTimeOffset.FromUnixTimeSeconds(issuedAt).DateTime;
// If the valid time is newer then when the token was issued, the token is not longer valid
if (user.TokenValidTimestamp > issuedAtDateTime)
return;
CurrentUserNullable = user;
if (CurrentUserNullable == null) // If the current user is null, stop loading additional data
return;
Flags = new(CurrentUser.Flags);
Permissions = new(CurrentUser.Permissions);
}
public async Task<string> Login(string email, string password, string? code = null)
{
var user = UserRepository
.Get()
.FirstOrDefault(x => x.Email == email);
if (user == null)
throw new DisplayException("A user with these credential combination was not found");
if (!HashHelper.Verify(password, user.Password))
throw new DisplayException("A user with these credential combination was not found");
var flags = new FlagStorage(user.Flags); // Construct FlagStorage to check for 2fa
if (!flags[UserFlag.TotpEnabled]) // No 2fa found on this user so were done here
return await GenerateToken(user);
// If we reach this point, 2fa is enabled so we need to continue validating
if (string.IsNullOrEmpty(code)) // This will show an additional 2fa login field
throw new ArgumentNullException(nameof(code), "2FA code missing");
if (user.TotpKey == null) // Hopefully we will never fulfill this check ;)
throw new DisplayException("2FA key is missing. Please contact the support to fix your account");
// Calculate server side code
var totp = new Totp(Base32Encoding.ToBytes(user.TotpKey));
var codeServerSide = totp.ComputeTotp();
if (codeServerSide == code)
return await GenerateToken(user);
throw new DisplayException("Invalid 2fa code entered");
}
public async Task<string> GenerateToken(User user)
{
var token = await JwtService.Create(data =>
{
data.Add("userId", user.Id.ToString());
data.Add("issuedAt", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString());
}, "User", TimeSpan.FromDays(10));
return token;
}
public Task SaveFlags()
{
// Prevent saving flags for an empty user
if (!IsSignedIn)
return Task.CompletedTask;
// Save the new flag string
CurrentUser.Flags = Flags.RawFlagString;
UserRepository.Update(CurrentUser);
return Task.CompletedTask;
}
// Helpers and overloads
public async Task
Authenticate(HttpRequest request) // Overload for api controllers to authenticate a user like the normal panel
{
if (request.Cookies.ContainsKey("token"))
{
var token = request.Cookies["token"];
await Authenticate(token!);
}
}
public async Task Authenticate(string token) // Overload to set token and reauth
{
Token = token;
await Authenticate();
}
}