2025-09-22 00:29:51 -05:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Net;
|
|
|
|
|
using System.Net.Http;
|
|
|
|
|
using System.Text.Json;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using LANCommander.SDK.Abstractions;
|
|
|
|
|
using LANCommander.SDK.Exceptions;
|
|
|
|
|
using LANCommander.SDK.Extensions;
|
|
|
|
|
using LANCommander.SDK.Factories;
|
|
|
|
|
using LANCommander.SDK.Models;
|
2026-07-23 19:47:22 -05:00
|
|
|
using LANCommander.SDK.Plugins;
|
|
|
|
|
using LANCommander.SDK.Plugins.Events;
|
2025-10-08 19:53:05 -05:00
|
|
|
using LANCommander.SDK.Providers;
|
2025-09-22 00:29:51 -05:00
|
|
|
using Microsoft.Extensions.Logging;
|
2025-10-08 19:53:05 -05:00
|
|
|
using AuthenticationProvider = LANCommander.SDK.Models.AuthenticationProvider;
|
2025-09-22 00:29:51 -05:00
|
|
|
|
|
|
|
|
namespace LANCommander.SDK.Services;
|
|
|
|
|
|
2025-09-24 20:58:23 -05:00
|
|
|
public class AuthenticationClient(
|
|
|
|
|
ILogger<AuthenticationClient> logger,
|
2025-09-22 00:29:51 -05:00
|
|
|
ITokenProvider tokenProvider,
|
2025-10-08 19:53:05 -05:00
|
|
|
IServerConfigurationRefresher configRefresher,
|
2025-11-07 20:19:13 -06:00
|
|
|
ISettingsProvider settingsProvider,
|
2025-09-22 00:29:51 -05:00
|
|
|
ApiRequestFactory apiRequestFactory,
|
2026-06-27 18:44:24 -05:00
|
|
|
IConnectionClient connectionClient,
|
2026-07-23 19:47:22 -05:00
|
|
|
ProfileClient profileClient,
|
|
|
|
|
IPluginEventBus pluginEventBus)
|
2025-09-22 00:29:51 -05:00
|
|
|
{
|
2025-10-01 18:45:30 -05:00
|
|
|
public async Task<AuthToken> AuthenticateAsync(string username, string password, Uri serverAddress)
|
2025-09-22 00:29:51 -05:00
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-10-01 18:45:30 -05:00
|
|
|
var result = await apiRequestFactory
|
2025-09-22 00:29:51 -05:00
|
|
|
.Create()
|
2025-10-01 18:45:30 -05:00
|
|
|
.UseBaseAddress(serverAddress)
|
2025-09-22 00:29:51 -05:00
|
|
|
.UseRoute("/api/Auth/Login")
|
|
|
|
|
.UseMethod(HttpMethod.Post)
|
|
|
|
|
.AddBody(new AuthRequest
|
|
|
|
|
{
|
|
|
|
|
UserName = username,
|
|
|
|
|
Password = password,
|
|
|
|
|
})
|
|
|
|
|
.SendAsync<AuthToken>();
|
|
|
|
|
|
|
|
|
|
ErrorResponse errorResponse = null;
|
|
|
|
|
|
2025-10-01 18:45:30 -05:00
|
|
|
if (!result.Response.IsSuccessStatusCode)
|
2025-09-22 00:29:51 -05:00
|
|
|
{
|
2025-10-01 18:45:30 -05:00
|
|
|
string message = result.Response.ReasonPhrase;
|
2025-09-22 00:29:51 -05:00
|
|
|
|
|
|
|
|
logger?.LogError("Authentication failed for user {UserName}: {Message}", username, message);
|
|
|
|
|
|
2025-10-01 18:45:30 -05:00
|
|
|
errorResponse = await ParseErrorResponseAsync(result.Response);
|
2025-09-22 00:29:51 -05:00
|
|
|
}
|
|
|
|
|
|
2025-10-01 18:45:30 -05:00
|
|
|
switch (result.Response.StatusCode)
|
2025-09-22 00:29:51 -05:00
|
|
|
{
|
|
|
|
|
case HttpStatusCode.OK:
|
|
|
|
|
var token = new AuthToken
|
|
|
|
|
{
|
2025-10-01 18:45:30 -05:00
|
|
|
AccessToken = result.Data.AccessToken,
|
|
|
|
|
RefreshToken = result.Data.RefreshToken,
|
|
|
|
|
Expiration = result.Data.Expiration
|
2025-09-22 00:29:51 -05:00
|
|
|
};
|
|
|
|
|
|
2025-11-19 00:36:03 -06:00
|
|
|
tokenProvider.SetToken(token);
|
2025-09-22 00:29:51 -05:00
|
|
|
|
2025-10-08 19:53:05 -05:00
|
|
|
await configRefresher.RefreshAsync();
|
|
|
|
|
|
2026-07-23 19:47:22 -05:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var profile = await profileClient.GetAsync(forceLoad: true);
|
|
|
|
|
|
|
|
|
|
if (profile != null)
|
|
|
|
|
await pluginEventBus.PublishAsync(new UserLoggedInEvent(profile.Id, profile.UserName));
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
logger?.LogWarning(ex, "Could not publish login event for user {UserName}", username);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-22 00:29:51 -05:00
|
|
|
return token;
|
|
|
|
|
|
|
|
|
|
case HttpStatusCode.Forbidden:
|
|
|
|
|
case HttpStatusCode.BadRequest:
|
|
|
|
|
case HttpStatusCode.Unauthorized:
|
|
|
|
|
logger?.LogError("Authentication failed for user {UserName}: invalid username or password", username);
|
2025-09-23 00:57:48 -05:00
|
|
|
throw new AuthFailedException(AuthFailedException.AuthenticationErrorCode.InvalidCredentials, "Invalid username or password", errorData: errorResponse);
|
2025-09-22 00:29:51 -05:00
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
logger?.LogError("Authentication failed for user {UserName}: could not communicate with the server", username);
|
|
|
|
|
throw new WebException("Could not communicate with the server");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
// OnError?.Invoke(this, ex);
|
|
|
|
|
|
|
|
|
|
throw;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task LogoutAsync()
|
|
|
|
|
{
|
2026-07-23 19:47:22 -05:00
|
|
|
User loggedOutUser = null;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
loggedOutUser = await profileClient.GetAsync();
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// Profile may be unavailable if the server is offline; logout still proceeds.
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-03 23:24:47 -06:00
|
|
|
try
|
|
|
|
|
{
|
2025-11-07 20:19:13 -06:00
|
|
|
await apiRequestFactory
|
2025-11-03 23:24:47 -06:00
|
|
|
.Create()
|
|
|
|
|
.UseRoute("/api/Auth/Logout")
|
|
|
|
|
.UseAuthenticationToken()
|
|
|
|
|
.PostAsync();
|
|
|
|
|
|
|
|
|
|
connectionClient.DisconnectAsync();
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
logger.LogWarning("Could not logout, server inaccessible");
|
|
|
|
|
}
|
2026-06-27 18:44:24 -05:00
|
|
|
|
2025-09-22 00:29:51 -05:00
|
|
|
tokenProvider.SetToken(null);
|
2026-06-27 18:44:24 -05:00
|
|
|
|
|
|
|
|
profileClient.ClearCache();
|
2026-07-23 19:47:22 -05:00
|
|
|
|
|
|
|
|
if (loggedOutUser != null)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await pluginEventBus.PublishAsync(new UserLoggedOutEvent(loggedOutUser.Id, loggedOutUser.UserName));
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
logger?.LogWarning(ex, "Could not publish logout event for user {UserName}", loggedOutUser.UserName);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-22 00:29:51 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task RegisterAsync(string username, string password, string passwordConfirmation)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-10-01 18:45:30 -05:00
|
|
|
var result = await apiRequestFactory
|
2025-09-22 00:29:51 -05:00
|
|
|
.Create()
|
|
|
|
|
.UseRoute("/api/Auth/Register")
|
|
|
|
|
.UseMethod(HttpMethod.Post)
|
|
|
|
|
.AddBody(new AuthRequest
|
|
|
|
|
{
|
|
|
|
|
UserName = username,
|
|
|
|
|
Password = password,
|
|
|
|
|
})
|
|
|
|
|
.SendAsync<AuthToken>();
|
|
|
|
|
|
|
|
|
|
ErrorResponse errorResponse = null;
|
|
|
|
|
|
2025-10-01 18:45:30 -05:00
|
|
|
if (!result.Response.IsSuccessStatusCode)
|
2025-09-22 00:29:51 -05:00
|
|
|
{
|
2025-10-01 18:45:30 -05:00
|
|
|
logger?.LogError("Registration failed for user {UserName}: {Message}", username, result.ErrorMessage);
|
2025-09-22 00:29:51 -05:00
|
|
|
|
2025-10-01 18:45:30 -05:00
|
|
|
errorResponse = await ParseErrorResponseAsync(result.Response);
|
2025-09-22 00:29:51 -05:00
|
|
|
}
|
|
|
|
|
|
2025-10-01 18:45:30 -05:00
|
|
|
switch (result.Response.StatusCode)
|
2025-09-22 00:29:51 -05:00
|
|
|
{
|
|
|
|
|
case HttpStatusCode.OK:
|
2025-11-19 00:36:03 -06:00
|
|
|
tokenProvider.SetToken(result.Data);
|
2025-09-22 00:29:51 -05:00
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
case HttpStatusCode.BadRequest:
|
|
|
|
|
case HttpStatusCode.Forbidden:
|
|
|
|
|
case HttpStatusCode.Unauthorized:
|
|
|
|
|
throw new RegisterFailedException("Could not register user", errorData: errorResponse);
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
logger?.LogError("Registering failed for user {UserName}: could not communicate with the server", username);
|
|
|
|
|
throw new WebException("Could not communicate with the server");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
// OnError?.Invoke(this, ex);
|
|
|
|
|
|
|
|
|
|
throw;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-24 20:58:23 -05:00
|
|
|
|
|
|
|
|
public async Task<bool> ValidateTokenAsync()
|
|
|
|
|
{
|
|
|
|
|
logger?.LogTrace("Validating token");
|
|
|
|
|
|
2025-11-19 00:36:03 -06:00
|
|
|
if (String.IsNullOrWhiteSpace(tokenProvider.GetToken()?.AccessToken))
|
2025-09-24 20:58:23 -05:00
|
|
|
{
|
|
|
|
|
logger?.LogError("Token is empty");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await apiRequestFactory
|
|
|
|
|
.Create()
|
|
|
|
|
.UseAuthenticationToken()
|
|
|
|
|
.UseRoute("/api/Auth/Validate")
|
2025-10-01 18:45:30 -05:00
|
|
|
.PostAsync();
|
2025-09-24 20:58:23 -05:00
|
|
|
|
|
|
|
|
logger?.LogTrace("Validated token successfully");
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
logger?.LogError("Validating token failed", ex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2025-09-22 00:29:51 -05:00
|
|
|
|
|
|
|
|
public async Task<IEnumerable<AuthenticationProvider>> GetAuthenticationProvidersAsync()
|
|
|
|
|
{
|
|
|
|
|
return await apiRequestFactory
|
|
|
|
|
.Create()
|
2026-05-12 19:57:43 -05:00
|
|
|
.UseRoute("/api/Auth/AuthenticationProviders")
|
2025-09-22 00:29:51 -05:00
|
|
|
.UseVersioning()
|
|
|
|
|
.GetAsync<IEnumerable<AuthenticationProvider>>();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-14 21:46:29 -05:00
|
|
|
public async Task<bool> GetRegistrationAllowedAsync()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var settings = await apiRequestFactory
|
|
|
|
|
.Create()
|
|
|
|
|
.UseRoute("/api/Settings")
|
|
|
|
|
.GetAsync<Settings>();
|
|
|
|
|
|
|
|
|
|
return settings?.Authentication?.AllowRegistration ?? true;
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// Older servers don't expose this setting - default to allowing registration
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 21:08:48 -05:00
|
|
|
public async Task<bool> GetAutoRedirectToProviderAsync()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var settings = await apiRequestFactory
|
|
|
|
|
.Create()
|
|
|
|
|
.UseRoute("/api/Settings")
|
|
|
|
|
.GetAsync<Settings>();
|
|
|
|
|
|
|
|
|
|
return settings?.Authentication?.AutoRedirectToProvider ?? false;
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// Older servers don't expose this setting - default to no auto-redirect
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-27 18:18:45 -05:00
|
|
|
public async Task<bool> GetEnableUserLibrariesAsync()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var settings = await apiRequestFactory
|
|
|
|
|
.Create()
|
|
|
|
|
.UseRoute("/api/Settings")
|
|
|
|
|
.GetAsync<Settings>();
|
|
|
|
|
|
|
|
|
|
return settings?.Library?.EnableUserLibraries ?? true;
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// Older servers don't expose this setting - default to enabling user libraries
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-22 00:29:51 -05:00
|
|
|
public Uri GetAuthenticationProviderLoginUrl(string provider)
|
|
|
|
|
{
|
2025-09-24 20:58:23 -05:00
|
|
|
return connectionClient.GetServerAddress().Join($"api/Auth/Login?Provider={provider}");
|
2025-09-22 00:29:51 -05:00
|
|
|
}
|
2026-05-12 17:21:53 -05:00
|
|
|
|
|
|
|
|
public Uri GetAuthenticationProviderLoginUrl(string provider, string requestId)
|
|
|
|
|
{
|
|
|
|
|
return connectionClient.GetServerAddress().Join($"api/Auth/Login?Provider={provider}&requestId={requestId}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<AuthToken?> RedeemTokenAsync(string code)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var result = await apiRequestFactory
|
|
|
|
|
.Create()
|
|
|
|
|
.UseRoute($"/api/Auth/Token/{code}")
|
|
|
|
|
.UseMethod(HttpMethod.Get)
|
|
|
|
|
.SendAsync<AuthToken>();
|
|
|
|
|
|
|
|
|
|
if (result.Response.IsSuccessStatusCode)
|
|
|
|
|
return result.Data;
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-22 00:29:51 -05:00
|
|
|
|
2025-09-23 00:57:48 -05:00
|
|
|
internal async Task<ErrorResponse> ParseErrorResponseAsync(HttpResponseMessage response, bool defaultToGenericResponse = false)
|
2025-09-22 00:29:51 -05:00
|
|
|
{
|
|
|
|
|
ErrorResponse errorResponse = null;
|
|
|
|
|
|
|
|
|
|
// Try to deserialize the error response.
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-09-23 00:57:48 -05:00
|
|
|
errorResponse = JsonSerializer.Deserialize<ErrorResponse>(await response.Content.ReadAsStringAsync());
|
2025-09-22 00:29:51 -05:00
|
|
|
|
|
|
|
|
return errorResponse;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception deserializationEx)
|
|
|
|
|
{
|
|
|
|
|
// Log error and create a fallback message if deserialization fails.
|
|
|
|
|
if (defaultToGenericResponse)
|
|
|
|
|
{
|
2025-09-23 00:57:48 -05:00
|
|
|
logger?.LogError(deserializationEx, "Error deserializing error response");
|
2025-09-22 00:29:51 -05:00
|
|
|
|
|
|
|
|
errorResponse = new ErrorResponse
|
|
|
|
|
{
|
|
|
|
|
Message = "Could not process the server response."
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return errorResponse;
|
|
|
|
|
}
|
|
|
|
|
}
|