LANCommander/LANCommander.SDK/Clients/AuthenticationClient.cs
2026-06-22 21:08:48 -05:00

289 lines
No EOL
9 KiB
C#

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;
using LANCommander.SDK.Providers;
using Microsoft.Extensions.Logging;
using AuthenticationProvider = LANCommander.SDK.Models.AuthenticationProvider;
namespace LANCommander.SDK.Services;
public class AuthenticationClient(
ILogger<AuthenticationClient> logger,
ITokenProvider tokenProvider,
IServerConfigurationRefresher configRefresher,
ISettingsProvider settingsProvider,
ApiRequestFactory apiRequestFactory,
IConnectionClient connectionClient)
{
public async Task<AuthToken> AuthenticateAsync(string username, string password, Uri serverAddress)
{
try
{
var result = await apiRequestFactory
.Create()
.UseBaseAddress(serverAddress)
.UseRoute("/api/Auth/Login")
.UseMethod(HttpMethod.Post)
.AddBody(new AuthRequest
{
UserName = username,
Password = password,
})
.SendAsync<AuthToken>();
ErrorResponse errorResponse = null;
if (!result.Response.IsSuccessStatusCode)
{
string message = result.Response.ReasonPhrase;
logger?.LogError("Authentication failed for user {UserName}: {Message}", username, message);
errorResponse = await ParseErrorResponseAsync(result.Response);
}
switch (result.Response.StatusCode)
{
case HttpStatusCode.OK:
var token = new AuthToken
{
AccessToken = result.Data.AccessToken,
RefreshToken = result.Data.RefreshToken,
Expiration = result.Data.Expiration
};
tokenProvider.SetToken(token);
await configRefresher.RefreshAsync();
return token;
case HttpStatusCode.Forbidden:
case HttpStatusCode.BadRequest:
case HttpStatusCode.Unauthorized:
logger?.LogError("Authentication failed for user {UserName}: invalid username or password", username);
throw new AuthFailedException(AuthFailedException.AuthenticationErrorCode.InvalidCredentials, "Invalid username or password", errorData: errorResponse);
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()
{
try
{
await apiRequestFactory
.Create()
.UseRoute("/api/Auth/Logout")
.UseAuthenticationToken()
.PostAsync();
connectionClient.DisconnectAsync();
}
catch (Exception ex)
{
logger.LogWarning("Could not logout, server inaccessible");
}
tokenProvider.SetToken(null);
}
public async Task RegisterAsync(string username, string password, string passwordConfirmation)
{
try
{
var result = await apiRequestFactory
.Create()
.UseRoute("/api/Auth/Register")
.UseMethod(HttpMethod.Post)
.AddBody(new AuthRequest
{
UserName = username,
Password = password,
})
.SendAsync<AuthToken>();
ErrorResponse errorResponse = null;
if (!result.Response.IsSuccessStatusCode)
{
logger?.LogError("Registration failed for user {UserName}: {Message}", username, result.ErrorMessage);
errorResponse = await ParseErrorResponseAsync(result.Response);
}
switch (result.Response.StatusCode)
{
case HttpStatusCode.OK:
tokenProvider.SetToken(result.Data);
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;
}
}
public async Task<bool> ValidateTokenAsync()
{
logger?.LogTrace("Validating token");
if (String.IsNullOrWhiteSpace(tokenProvider.GetToken()?.AccessToken))
{
logger?.LogError("Token is empty");
return false;
}
try
{
await apiRequestFactory
.Create()
.UseAuthenticationToken()
.UseRoute("/api/Auth/Validate")
.PostAsync();
logger?.LogTrace("Validated token successfully");
return true;
}
catch (Exception ex)
{
logger?.LogError("Validating token failed", ex);
}
return false;
}
public async Task<IEnumerable<AuthenticationProvider>> GetAuthenticationProvidersAsync()
{
return await apiRequestFactory
.Create()
.UseRoute("/api/Auth/AuthenticationProviders")
.UseVersioning()
.GetAsync<IEnumerable<AuthenticationProvider>>();
}
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;
}
}
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;
}
}
public Uri GetAuthenticationProviderLoginUrl(string provider)
{
return connectionClient.GetServerAddress().Join($"api/Auth/Login?Provider={provider}");
}
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;
}
}
internal async Task<ErrorResponse> ParseErrorResponseAsync(HttpResponseMessage response, bool defaultToGenericResponse = false)
{
ErrorResponse errorResponse = null;
// Try to deserialize the error response.
try
{
errorResponse = JsonSerializer.Deserialize<ErrorResponse>(await response.Content.ReadAsStringAsync());
return errorResponse;
}
catch (Exception deserializationEx)
{
// Log error and create a fallback message if deserialization fails.
if (defaultToGenericResponse)
{
logger?.LogError(deserializationEx, "Error deserializing error response");
errorResponse = new ErrorResponse
{
Message = "Could not process the server response."
};
}
}
return errorResponse;
}
}