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;
}
}
}