using DiunaBI.UI.Shared.Services; using Microsoft.Maui.Authentication; using System.Text.Json; namespace DiunaBI.UI.Mobile.Services; /// /// Mobile implementation of Google authentication using WebAuthenticator /// Opens OAuth in native browser /// public class MobileGoogleAuthService : IGoogleAuthService { private readonly AuthService _authService; private const string GoogleClientId = "784258364493-t8g2bq0utgm9ac3pr8umov6i76uls65s.apps.googleusercontent.com"; private const string RedirectUri = "com.diunabi:/oauth2redirect"; public MobileGoogleAuthService(AuthService authService) { _authService = authService; } public async Task SignInAsync() { try { Console.WriteLine("🔐 Starting mobile Google authentication..."); // Build Google OAuth URL - use code flow with iOS URL scheme var state = Guid.NewGuid().ToString("N"); var codeVerifier = GenerateCodeVerifier(); var codeChallenge = GenerateCodeChallenge(codeVerifier); var authUrl = "https://accounts.google.com/o/oauth2/v2/auth" + $"?client_id={GoogleClientId}" + $"&redirect_uri={Uri.EscapeDataString(RedirectUri)}" + $"&response_type=code" + $"&scope={Uri.EscapeDataString("openid profile email")}" + $"&state={state}" + $"&code_challenge={codeChallenge}" + $"&code_challenge_method=S256"; Console.WriteLine($"📱 Opening browser for Google OAuth..."); var result = await WebAuthenticator.Default.AuthenticateAsync( new Uri(authUrl), new Uri(RedirectUri)); Console.WriteLine($"✅ Got response from Google OAuth"); // Extract authorization code if (result.Properties.TryGetValue("code", out var code)) { Console.WriteLine($"✅ Got authorization code"); // Exchange code for ID token var idToken = await ExchangeCodeForToken(code, codeVerifier); if (string.IsNullOrEmpty(idToken)) { Console.WriteLine("❌ Failed to exchange code for token"); return false; } Console.WriteLine($"✅ Got ID token, length: {idToken.Length}"); // Decode the JWT to get user info var userInfo = DecodeJwtPayload(idToken); var root = userInfo.RootElement; var fullName = root.GetProperty("name").GetString() ?? ""; var email = root.GetProperty("email").GetString() ?? ""; var avatarUrl = root.TryGetProperty("picture", out var pic) ? pic.GetString() ?? "" : ""; Console.WriteLine($"👤 User: {fullName} ({email})"); // Validate with backend (bool success, string? errorMessage) = await _authService.ValidateWithBackendAsync( idToken, fullName, email, avatarUrl); if (success) { Console.WriteLine("✅ Backend validation successful"); return true; } else { Console.WriteLine($"❌ Backend validation failed: {errorMessage}"); return false; } } Console.WriteLine("❌ No authorization code in OAuth response"); return false; } catch (TaskCanceledException) { Console.WriteLine("â„šī¸ User cancelled authentication"); return false; } catch (Exception ex) { Console.WriteLine($"❌ Mobile authentication error: {ex.Message}"); Console.WriteLine($"Stack: {ex.StackTrace}"); return false; } } private async Task ExchangeCodeForToken(string code, string codeVerifier) { try { using var httpClient = new HttpClient(); var tokenRequest = new Dictionary { { "code", code }, { "client_id", GoogleClientId }, { "redirect_uri", RedirectUri }, { "grant_type", "authorization_code" }, { "code_verifier", codeVerifier } }; var response = await httpClient.PostAsync( "https://oauth2.googleapis.com/token", new FormUrlEncodedContent(tokenRequest)); if (!response.IsSuccessStatusCode) { var error = await response.Content.ReadAsStringAsync(); Console.WriteLine($"❌ Token exchange failed: {error}"); return null; } var json = await response.Content.ReadAsStringAsync(); var doc = JsonDocument.Parse(json); return doc.RootElement.GetProperty("id_token").GetString(); } catch (Exception ex) { Console.WriteLine($"❌ Error exchanging code: {ex.Message}"); return null; } } private string GenerateCodeVerifier() { var bytes = new byte[32]; System.Security.Cryptography.RandomNumberGenerator.Fill(bytes); return Convert.ToBase64String(bytes) .TrimEnd('=') .Replace('+', '-') .Replace('/', '_'); } private string GenerateCodeChallenge(string codeVerifier) { using var sha256 = System.Security.Cryptography.SHA256.Create(); var bytes = System.Text.Encoding.ASCII.GetBytes(codeVerifier); var hash = sha256.ComputeHash(bytes); return Convert.ToBase64String(hash) .TrimEnd('=') .Replace('+', '-') .Replace('/', '_'); } private JsonDocument DecodeJwtPayload(string token) { try { var parts = token.Split('.'); if (parts.Length != 3) throw new Exception("Invalid JWT format"); var payload = parts[1]; // Add padding if needed var paddingNeeded = (4 - (payload.Length % 4)) % 4; payload += new string('=', paddingNeeded); // Convert from base64url to base64 payload = payload.Replace('-', '+').Replace('_', '/'); var jsonBytes = Convert.FromBase64String(payload); return JsonDocument.Parse(jsonBytes); } catch (Exception ex) { Console.WriteLine($"❌ Error decoding JWT: {ex.Message}"); throw; } } }