Files
DiunaBI/DiunaBI.UI.Mobile/Services/MobileGoogleAuthService.cs

193 lines
6.6 KiB
C#
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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