//-----------------------------------------------------------------------
//
// Copyright (c) aliasvault. All rights reserved.
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
//
//-----------------------------------------------------------------------
namespace AliasVault.Auth;
using AliasServerDb;
using AliasVault.Shared.Models.Configuration;
using AliasVault.Shared.Models.Enums;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
///
/// Auth logging service for logging authentication events such as user login attempts.
///
/// IServiceProvider instance.
/// IHttpContextAccessor instance.
public class AuthLoggingService(IServiceProvider serviceProvider, IHttpContextAccessor httpContextAccessor)
{
///
/// Logs a successful auth event.
///
/// Username of login attempt.
/// The type of auth event.
public async Task LogAuthEventSuccessAsync(string username, AuthEventType eventType)
{
using var scope = serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService();
var httpContext = httpContextAccessor.HttpContext;
var clientHeader = httpContext?.Request.Headers["X-AliasVault-Client"].FirstOrDefault();
var config = scope.ServiceProvider.GetRequiredService();
var ipAddress = config.IpLoggingEnabled ? IpAddressUtility.GetIpFromContext(httpContext) : "xxx.xxx.xxx.xxx";
var authAttempt = new AuthLog
{
Timestamp = DateTime.UtcNow,
Username = username,
EventType = eventType,
IsSuccess = true,
FailureReason = null,
IpAddress = ipAddress,
Client = clientHeader,
RequestPath = httpContext?.Request.Path,
DeviceType = DetermineDeviceType(httpContext),
OperatingSystem = DetermineOperatingSystem(httpContext),
Browser = DetermineBrowser(httpContext),
Country = DetermineCountry(),
IsSuspiciousActivity = false,
};
dbContext.AuthLogs.Add(authAttempt);
// Update user's last activity date.
var user = await dbContext.AliasVaultUsers.FirstOrDefaultAsync(u => u.UserName == username);
if (user != null)
{
user.LastActivityDate = DateTime.UtcNow;
}
await dbContext.SaveChangesAsync();
}
///
/// Logs an unsuccessful (failed) authentication attempt.
///
/// Username of login attempt.
/// The type of auth event.
/// Reason of failure. Defaults to AuthFailureReason.None to indicate success.
public async Task LogAuthEventFailAsync(string username, AuthEventType eventType, AuthFailureReason failureReason)
{
using var scope = serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService();
var httpContext = httpContextAccessor.HttpContext;
var clientHeader = httpContext?.Request.Headers["X-AliasVault-Client"].FirstOrDefault();
var config = httpContext?.RequestServices.GetService();
var ipAddress = config?.IpLoggingEnabled == true ? IpAddressUtility.GetIpFromContext(httpContext) : "xxx.xxx.xxx.xxx";
var authAttempt = new AuthLog
{
Timestamp = DateTime.UtcNow,
Username = username,
EventType = eventType,
IsSuccess = false,
FailureReason = failureReason,
IpAddress = ipAddress,
Client = clientHeader,
RequestPath = httpContext?.Request.Path,
DeviceType = DetermineDeviceType(httpContext),
OperatingSystem = DetermineOperatingSystem(httpContext),
Browser = DetermineBrowser(httpContext),
Country = DetermineCountry(),
IsSuspiciousActivity = false,
};
dbContext.AuthLogs.Add(authAttempt);
await dbContext.SaveChangesAsync();
}
///
/// Determines the type of device based on the User-Agent header.
///
/// The HttpContext containing the request information.
/// A string representing the device type: "Mobile", "Tablet", "Smart TV", "Desktop", or "Unknown".
private static string? DetermineDeviceType(HttpContext? context)
{
if (context is null)
{
return null;
}
return context.Request.Headers.UserAgent.ToString().ToLower() switch
{
var ua when ua.Contains("mobile") || ua.Contains("android") || ua.Contains("iphone") => "Mobile",
var ua when ua.Contains("tablet") || ua.Contains("ipad") => "Tablet",
var ua when ua.Contains("tv") || ua.Contains("smart-tv") => "Smart TV",
_ => "Desktop"
};
}
///
/// Determines the operating system based on the User-Agent header.
///
/// The HttpContext containing the request information.
/// A string representing the operating system: "Windows", "MacOS", "Linux", "Android", "iOS", or "Unknown".
private static string? DetermineOperatingSystem(HttpContext? context)
{
if (context is null)
{
return null;
}
return context.Request.Headers.UserAgent.ToString().ToLower() switch
{
var ua when ua.Contains("win") => "Windows",
var ua when ua.Contains("mac") => "MacOS",
var ua when ua.Contains("linux") => "Linux",
var ua when ua.Contains("android") => "Android",
var ua when ua.Contains("iphone") || ua.Contains("ipad") => "iOS",
_ => null,
};
}
///
/// Determines the browser type based on the User-Agent header.
///
/// The HttpContext containing the request information.
/// A string representing the browser: "Firefox", "Chrome", "Safari", "Edge", "Opera", or "Unknown".
private static string? DetermineBrowser(HttpContext? context)
{
if (context is null)
{
return null;
}
return context.Request.Headers.UserAgent.ToString().ToLower() switch
{
var ua when ua.Contains("firefox") => "Firefox",
var ua when ua.Contains("chrome") && !ua.Contains("edg") => "Chrome",
var ua when ua.Contains("safari") && !ua.Contains("chrome") => "Safari",
var ua when ua.Contains("edg") => "Edge",
var ua when ua.Contains("opr") || ua.Contains("opera") => "Opera",
_ => null
};
}
///
/// Determines the country based on the IP address of the request.
///
/// A string representing the country or "Unknown" if the country cannot be determined.
///
/// This method currently returns null as the implementation is not yet complete.
///
private static string? DetermineCountry()
{
// Implement later by using a Geo-IP database or service.
return null;
}
}