using Avalonia_Services.Services.AuthService; using System; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Authentication { public sealed class PcGlobalTokenService(IPcThirdPartyAuthorizationClient thirdPartyClient) { private static readonly string[] SuperRoles = ["SuperAdmin", "Admin"]; private readonly object _syncRoot = new(); private PcTokenState? _current; private static readonly TimeSpan NormalLifetime = TimeSpan.FromHours(8); private static readonly TimeSpan TemporaryFailureLifetime = TimeSpan.FromMinutes(20); private static readonly TimeSpan MaxTemporaryFailureWindow = TimeSpan.FromHours(24); public async Task AuthorizeAsync(string? authorizationCode, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(authorizationCode)) { return null; } var result = await thirdPartyClient.ValidateAuthorizationCodeAsync(authorizationCode, cancellationToken); if (result != ThirdPartyAuthCheckResult.Valid) { return null; } return IssueToken(authorizationCode, NormalLifetime, resetTemporaryFailureWindow: true); } public async Task RefreshAsync(string? token, CancellationToken cancellationToken = default) { PcTokenState? current; lock (_syncRoot) { current = IsCurrentToken(token) ? _current : null; } if (current is null) { return null; } var result = await thirdPartyClient.RefreshAuthorizationAsync(current.AuthorizationReference, cancellationToken); return result switch { ThirdPartyAuthCheckResult.Valid => IssueToken(current.AuthorizationReference, NormalLifetime, resetTemporaryFailureWindow: true), ThirdPartyAuthCheckResult.AuthorizationLost => ClearAndReturnNull(), ThirdPartyAuthCheckResult.TemporaryFailure => RefreshAfterTemporaryFailure(current), _ => null, }; } public async Task ValidateAsync(string? token, CancellationToken cancellationToken = default) { PcTokenState? current; lock (_syncRoot) { if (!IsCurrentToken(token)) { return false; } current = _current; if (current is not null && current.ExpiresAt > DateTime.UtcNow) { return true; } } return await RefreshAsync(token, cancellationToken) is not null; } public void Logout(string? token) { lock (_syncRoot) { if (IsCurrentToken(token)) { _current = null; } } } private PcTokenResponse IssueToken(string authorizationReference, TimeSpan lifetime, bool resetTemporaryFailureWindow) { var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)); var now = DateTime.UtcNow; var state = new PcTokenState( HashToken(token), authorizationReference, now.Add(lifetime), resetTemporaryFailureWindow ? null : _current?.TemporaryFailureStartedAt ?? now); lock (_syncRoot) { _current = state; } return new PcTokenResponse(token, state.ExpiresAt, SuperRoles); } private PcTokenResponse? RefreshAfterTemporaryFailure(PcTokenState current) { var startedAt = current.TemporaryFailureStartedAt ?? DateTime.UtcNow; if (DateTime.UtcNow - startedAt > MaxTemporaryFailureWindow) { return ClearAndReturnNull(); } return IssueToken(current.AuthorizationReference, TemporaryFailureLifetime, resetTemporaryFailureWindow: false); } private PcTokenResponse? ClearAndReturnNull() { lock (_syncRoot) { _current = null; } return null; } private bool IsCurrentToken(string? token) { return !string.IsNullOrWhiteSpace(token) && _current is not null && string.Equals(_current.TokenHash, HashToken(token), StringComparison.Ordinal); } private static string HashToken(string token) { var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(token)); return Convert.ToHexString(bytes); } private sealed record PcTokenState( string TokenHash, string AuthorizationReference, DateTime ExpiresAt, DateTime? TemporaryFailureStartedAt); } }