193 lines
6.6 KiB
C#
193 lines
6.6 KiB
C#
using DiunaBI.UI.Shared.Services;
|
||
using Microsoft.Maui.Authentication;
|
||
using System.Text.Json;
|
||
|
||
namespace DiunaBI.UI.Mobile.Services;
|
||
|
||
/// <summary>
|
||
/// Mobile implementation of Google authentication using WebAuthenticator
|
||
/// Opens OAuth in native browser
|
||
/// </summary>
|
||
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<bool> 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<string?> ExchangeCodeForToken(string code, string codeVerifier)
|
||
{
|
||
try
|
||
{
|
||
using var httpClient = new HttpClient();
|
||
|
||
var tokenRequest = new Dictionary<string, string>
|
||
{
|
||
{ "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;
|
||
}
|
||
}
|
||
}
|