using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Identity.Client; using Microsoft.Identity.Client.Desktop; namespace TestApp { public class AuthService : DisposableObject, IAuthTokenService { private readonly string _clientId = "XXXXXXXXXXXXXXXXXXXX"; private readonly string _tenantId = "YYYYYYYYYYYYYYYYYYYYYYYYY"; private readonly string[] _scopes = new[] { "User.Read" }; private readonly ILogger _logger; private readonly IPublicClientApplication _msalApp; private readonly int _cacheExpireMinutes = 5; private Timer _refreshTimer; public event EventHandler TokenRefreshed; public event EventHandler TokenRefreshFailed; public event EventHandler ReauthenticationRequired; public event EventHandler SignedOut; public event EventHandler SignInFailed; public event EventHandler SignedIn; public AuthService(ILogger logger) { _logger = logger; _msalApp = PublicClientApplicationBuilder .Create(_clientId) .WithAuthority(AzureCloudInstance.AzurePublic, _tenantId) .WithDefaultRedirectUri() // instead of "http://localhost" .WithWindowsEmbeddedBrowserSupport() .Build(); } private AuthenticationResult _cachedResult; public string CurrentUsername => _cachedResult?.Account?.Username; public string AccessToken => _cachedResult?.AccessToken; public DateTimeOffset? TokenExpiresOn => _cachedResult?.ExpiresOn; public bool IsTokenValid => _cachedResult != null && _cachedResult.ExpiresOn > DateTimeOffset.UtcNow.AddMinutes(_cacheExpireMinutes); public string GetCurrentUserObjectId() => _cachedResult?.ClaimsPrincipal?.FindFirst("oid")?.Value; public async Task IsSignedInAsync() { var accounts = await _msalApp.GetAccountsAsync(); return accounts.Any(); } public async Task SignInAsync() { // Use cached token if still valid if (_cachedResult != null && _cachedResult.ExpiresOn > DateTimeOffset.UtcNow.AddMinutes(_cacheExpireMinutes)) { return _cachedResult; } // Try silent first (if we have any account cached) try { var accounts = await _msalApp.GetAccountsAsync().ConfigureAwait(false); if (accounts.Any()) { _logger?.LogInformation("MSAL: attempting silent token acquisition for {User}", accounts.First().Username); _cachedResult = await _msalApp .AcquireTokenSilent(_scopes, accounts.First()) .ExecuteAsync() .ConfigureAwait(false); StartRefreshTimer(); OnSignedIn(); return _cachedResult; } } catch (MsalUiRequiredException) { _logger?.LogInformation("MSAL: silent token not available; proceeding to interactive."); } catch (Exception ex) { _logger?.LogError(ex, "Sign-in (silent) failed."); OnSignInFailed(ex); throw new Exception("Sign-in (silent) failed", ex); } // Interactive with embedded web view, short deadline, and system-browser fallback var sw = System.Diagnostics.Stopwatch.StartNew(); // Mitigate blank/white embedded window on some machines try { var existingArgs = Environment.GetEnvironmentVariable("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS", EnvironmentVariableTarget.Process); var hardenedArgs = "--disable-gpu --disable-features=RendererCodeIntegrity"; if (string.IsNullOrWhiteSpace(existingArgs)) Environment.SetEnvironmentVariable("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS", hardenedArgs, EnvironmentVariableTarget.Process); else if (!existingArgs.Contains("--disable-gpu") || !existingArgs.Contains("RendererCodeIntegrity")) Environment.SetEnvironmentVariable("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS", existingArgs + " " + hardenedArgs, EnvironmentVariableTarget.Process); } catch { /* non-fatal */ } try { _logger?.LogInformation("MSAL: starting interactive sign-in using embedded web view."); using var embeddedCts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); // short deadline _cachedResult = await _msalApp .AcquireTokenInteractive(_scopes) .WithPrompt(Prompt.SelectAccount) .WithAccount(null) .WithUseEmbeddedWebView(true) .WithEmbeddedWebViewOptions(new EmbeddedWebViewOptions { Title = "Sign in to E-Manager 2" }) .ExecuteAsync(embeddedCts.Token) .ConfigureAwait(false); _logger?.LogInformation("MSAL: embedded sign-in completed in {ms} ms", sw.ElapsedMilliseconds); } catch (OperationCanceledException oce) { _logger?.LogWarning(oce, "MSAL: embedded did not paint within deadline ({ms} ms). Falling back to system browser.", sw.ElapsedMilliseconds); using var browserCts = new CancellationTokenSource(TimeSpan.FromMinutes(2)); // generous time for browser _cachedResult = await _msalApp .AcquireTokenInteractive(_scopes) .WithPrompt(Prompt.SelectAccount) .WithAccount(null) .WithUseEmbeddedWebView(false) // system browser .ExecuteAsync(browserCts.Token) .ConfigureAwait(false); _logger?.LogInformation("MSAL: system-browser sign-in completed in {ms} ms", sw.ElapsedMilliseconds); } catch (MsalClientException ex) when (ex.ErrorCode == "authentication_ui_failed" || ex.ErrorCode == "unknown_error") { _logger?.LogWarning(ex, "MSAL: embedded failed quickly ({ms} ms). Falling back to system browser.", sw.ElapsedMilliseconds); using var browserCts = new CancellationTokenSource(TimeSpan.FromMinutes(2)); _cachedResult = await _msalApp .AcquireTokenInteractive(_scopes) .WithPrompt(Prompt.SelectAccount) .WithAccount(null) .WithUseEmbeddedWebView(false) .ExecuteAsync(browserCts.Token) .ConfigureAwait(false); _logger?.LogInformation("MSAL: system-browser sign-in completed in {ms} ms", sw.ElapsedMilliseconds); } catch (Exception ex) { _logger?.LogError(ex, "Sign-in (interactive) failed."); OnSignInFailed(ex); throw new Exception("Sign-in (interactive) failed", ex); } StartRefreshTimer(); OnSignedIn(); return _cachedResult; } public async Task GetAccessTokenAsync() { var result = await SignInAsync(); return result.AccessToken; } public async Task GetSignedInUsernameAsync() { var result = await SignInAsync(); return result.Account.Username; } public async Task SignOutAsync() { var accounts = await _msalApp.GetAccountsAsync(); foreach (var acc in accounts) await _msalApp.RemoveAsync(acc); _cachedResult = null; OnSignedOut(); } private async Task RefreshTokenAsync() { try { var accounts = await _msalApp.GetAccountsAsync(); if (!accounts.Any()) return; _cachedResult = await _msalApp.AcquireTokenSilent(_scopes, accounts.First()) .ExecuteAsync(); OnTokenRefreshed(); StartRefreshTimer(); // reset timer for new expiration } catch (MsalUiRequiredException) { OnReauthenticationRequired(); } catch (Exception ex) { OnTokenRefreshFailed(ex); } } private void StartRefreshTimer() { StopRefreshTimer(); var now = DateTimeOffset.UtcNow; var refreshTime = _cachedResult.ExpiresOn - now - TimeSpan.FromMinutes(_cacheExpireMinutes); if (refreshTime <= TimeSpan.Zero) refreshTime = TimeSpan.FromSeconds(10); // fallback to short delay _refreshTimer = new Timer(_ => { _ = Task.Run(async () => { try { await RefreshTokenAsync(); } catch (Exception ex) { OnTokenRefreshFailed(ex); } }); }, null, refreshTime, Timeout.InfiniteTimeSpan); } private void StopRefreshTimer() { _refreshTimer?.Dispose(); _refreshTimer = null; } protected override void DisposeManagedResources() { StopRefreshTimer(); } protected virtual void OnSignedIn() { if (_cachedResult == null) return; SignedIn?.Invoke(this, new SignedInEventArgs( _cachedResult.Account?.Username, _cachedResult.ExpiresOn )); } protected virtual void OnTokenRefreshed() { if (_cachedResult == null) return; TokenRefreshed?.Invoke(this, new TokenRefreshedEventArgs(_cachedResult.ExpiresOn, _cachedResult.Account?.Username)); } protected virtual void OnTokenRefreshFailed(Exception ex) => TokenRefreshFailed?.Invoke(this, new TokenRefreshFailedEventArgs(ex)); protected virtual void OnReauthenticationRequired() => ReauthenticationRequired?.Invoke(this, EventArgs.Empty); protected virtual void OnSignedOut() => SignedOut?.Invoke(this, EventArgs.Empty); protected virtual void OnSignInFailed(Exception ex) => SignInFailed?.Invoke(this, new SignInFailedEventArgs(ex)); public string GetToken() { return _cachedResult.Account?.Token; } } }