From 8077826c46e0f42b78a26bf4442c084720f4adb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Zieli=C5=84ski?= Date: Sun, 23 Nov 2025 15:17:54 +0100 Subject: [PATCH] WIP: ios loggin --- .../DiunaBI.UI.Mobile.csproj | 4 +- src/Backend/DiunaBI.UI.Mobile/MauiProgram.cs | 2 + .../Platforms/iOS/Info.plist | 13 ++ .../Services/MobileGoogleAuthService.cs | 192 ++++++++++++++++++ .../Components/LoginCard.razor | 38 +++- .../Services/IGoogleAuthService.cs | 13 ++ .../Services/WebGoogleAuthService.cs | 30 +++ 7 files changed, 288 insertions(+), 4 deletions(-) create mode 100644 src/Backend/DiunaBI.UI.Mobile/Services/MobileGoogleAuthService.cs create mode 100644 src/Backend/DiunaBI.UI.Shared/Services/IGoogleAuthService.cs create mode 100644 src/Backend/DiunaBI.UI.Shared/Services/WebGoogleAuthService.cs diff --git a/src/Backend/DiunaBI.UI.Mobile/DiunaBI.UI.Mobile.csproj b/src/Backend/DiunaBI.UI.Mobile/DiunaBI.UI.Mobile.csproj index 514e0f2..e912b5a 100644 --- a/src/Backend/DiunaBI.UI.Mobile/DiunaBI.UI.Mobile.csproj +++ b/src/Backend/DiunaBI.UI.Mobile/DiunaBI.UI.Mobile.csproj @@ -26,7 +26,7 @@ DiunaBI - com.bimit.diunabi + com.diunabi 1.0 @@ -41,7 +41,7 @@ - com.bimit.diunabi + com.diunabi Apple Development: Michal Zielinski (2F35ZHMBTB) bimai-local ios-arm64 diff --git a/src/Backend/DiunaBI.UI.Mobile/MauiProgram.cs b/src/Backend/DiunaBI.UI.Mobile/MauiProgram.cs index d62a5b5..c3a42a8 100644 --- a/src/Backend/DiunaBI.UI.Mobile/MauiProgram.cs +++ b/src/Backend/DiunaBI.UI.Mobile/MauiProgram.cs @@ -1,5 +1,6 @@ using System.Reflection; using DiunaBI.UI.Mobile; +using DiunaBI.UI.Mobile.Services; using DiunaBI.UI.Shared.Extensions; using DiunaBI.UI.Shared.Services; using Microsoft.Extensions.Configuration; @@ -30,6 +31,7 @@ public static class MauiProgram builder.Services.AddMudServices(); builder.Services.AddScoped(); + builder.Services.AddScoped(); var baseUrl = GetApiBaseUrl(); builder.Services.AddSharedServices(baseUrl); diff --git a/src/Backend/DiunaBI.UI.Mobile/Platforms/iOS/Info.plist b/src/Backend/DiunaBI.UI.Mobile/Platforms/iOS/Info.plist index 4edd2c1..4b219fd 100644 --- a/src/Backend/DiunaBI.UI.Mobile/Platforms/iOS/Info.plist +++ b/src/Backend/DiunaBI.UI.Mobile/Platforms/iOS/Info.plist @@ -30,5 +30,18 @@ Assets.xcassets/appicon.appiconset NSCameraUsageDescription Ta aplikacja potrzebuje dostępu do kamery w celu skanowania kodów EAN produktów. + CFBundleURLTypes + + + CFBundleURLName + com.diunabi + CFBundleURLSchemes + + com.diunabi + + CFBundleTypeRole + Editor + + diff --git a/src/Backend/DiunaBI.UI.Mobile/Services/MobileGoogleAuthService.cs b/src/Backend/DiunaBI.UI.Mobile/Services/MobileGoogleAuthService.cs new file mode 100644 index 0000000..14786e4 --- /dev/null +++ b/src/Backend/DiunaBI.UI.Mobile/Services/MobileGoogleAuthService.cs @@ -0,0 +1,192 @@ +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; + } + } +} diff --git a/src/Backend/DiunaBI.UI.Shared/Components/LoginCard.razor b/src/Backend/DiunaBI.UI.Shared/Components/LoginCard.razor index 20a5674..5e1572f 100644 --- a/src/Backend/DiunaBI.UI.Shared/Components/LoginCard.razor +++ b/src/Backend/DiunaBI.UI.Shared/Components/LoginCard.razor @@ -5,6 +5,7 @@ @inject IConfiguration Configuration @inject AuthService AuthService @inject NavigationManager NavigationManager +@inject IGoogleAuthService? GoogleAuthService