Fixes the Android Login process
This commit is contained in:
parent
dd5904f0a7
commit
75b298b0f8
16 changed files with 106 additions and 129 deletions
14
.vscode/launch.json
vendored
14
.vscode/launch.json
vendored
|
|
@ -5,19 +5,19 @@
|
|||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Android",
|
||||
"name": "Android Debug",
|
||||
"type": "mono",
|
||||
"preLaunchTask": "run-debug-android",
|
||||
"request": "attach",
|
||||
"address": "localhost",
|
||||
"port": 10000
|
||||
"port": 55555
|
||||
},
|
||||
{
|
||||
"name": "Attach - Android",
|
||||
"name": "Android Attach - Debug",
|
||||
"type": "mono",
|
||||
"request": "attach",
|
||||
"address": "localhost",
|
||||
"port": 10000
|
||||
"port": 55555
|
||||
},
|
||||
{
|
||||
"name": "API",
|
||||
|
|
@ -26,19 +26,19 @@
|
|||
"projectPath": "${workspaceFolder}/src/Api/Api.csproj"
|
||||
},
|
||||
{
|
||||
"name": "Yavsc.Org",
|
||||
"name": "Yavsc Org",
|
||||
"type": "dotnet",
|
||||
"request": "launch",
|
||||
"projectPath": "${workspaceFolder}/src/Yavsc.Org/Yavsc.Org.csproj",
|
||||
},
|
||||
{
|
||||
"name": "Yavsc.Blogs",
|
||||
"name": "Yavsc Blogs",
|
||||
"type": "dotnet",
|
||||
"request": "launch",
|
||||
"projectPath": "${workspaceFolder}/src/Yavsc.Blogs/Yavsc.Blogs.csproj"
|
||||
},
|
||||
{
|
||||
"name": "PostIt",
|
||||
"name": "PostIt Desktop",
|
||||
"type": "dotnet",
|
||||
"request": "launch",
|
||||
"projectPath": "${workspaceFolder}/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj",
|
||||
|
|
|
|||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
|
@ -5,6 +5,7 @@
|
|||
"appsettings",
|
||||
"asciidoctor",
|
||||
"ASPNETCORE",
|
||||
"Avalonia",
|
||||
"Configurabilité",
|
||||
"Cratie",
|
||||
"DESTDIR",
|
||||
|
|
@ -15,6 +16,7 @@
|
|||
"Hsts",
|
||||
"Newtonsoft",
|
||||
"Npgsql",
|
||||
"Oidc",
|
||||
"PKCE",
|
||||
"postit",
|
||||
"pschneider",
|
||||
|
|
|
|||
4
.vscode/tasks.json
vendored
4
.vscode/tasks.json
vendored
|
|
@ -18,8 +18,8 @@
|
|||
"-p:TargetFramework=net10.0-android",
|
||||
"-p:Configuration=Debug",
|
||||
"-p:AndroidAttachDebugger=true",
|
||||
"-p:AndroidSdbHostPort=10000",
|
||||
"-p:AndroidSdbTargetPort=10000"
|
||||
"-p:AndroidSdbHostPort=55555",
|
||||
"-p:AndroidSdbTargetPort=55555"
|
||||
],
|
||||
"problemMatcher": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
using Android;
|
||||
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using AndroidX.Core.Provider;
|
||||
using AndroidX.Emoji2.Text;
|
||||
using Avalonia;
|
||||
using Avalonia.Android;
|
||||
using AndroidX.Core.Provider;
|
||||
using AndroidX.Emoji2.Text;
|
||||
using PostIt.Droid.Services;
|
||||
|
||||
namespace PostIt.Android;
|
||||
|
||||
|
|
@ -34,7 +32,7 @@ public class MainActivity : AvaloniaMainActivity
|
|||
Yavsc.Resource.Array.com_google_android_gms_fonts_certs); //com_google_android_gms_fonts_certs
|
||||
EmojiCompat.Config config = new FontRequestEmojiCompatConfig(this, fontRequest);
|
||||
EmojiCompat.Init(config);
|
||||
PlatformBootstrap.EnsureInitialized();
|
||||
PlatformBootstrap.InitPlatform();
|
||||
base.OnCreate(savedInstanceState);
|
||||
Current = this;
|
||||
}
|
||||
|
|
@ -50,7 +48,13 @@ public class MainActivity : AvaloniaMainActivity
|
|||
protected override void OnNewIntent(Intent? intent)
|
||||
{
|
||||
base.OnNewIntent(intent);
|
||||
if (intent is not null) AndroidOidcCallbackSink.Handle(intent);
|
||||
|
||||
var url = intent?.DataString;
|
||||
if (!string.IsNullOrEmpty(url) && url.StartsWith("postit://callback"))
|
||||
{
|
||||
OidcCallbackManager.SetResult(url);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal static class AndroidOidcCallbackSink
|
||||
|
|
|
|||
|
|
@ -12,14 +12,9 @@ namespace PostIt.Android;
|
|||
/// </summary>
|
||||
internal static class PlatformBootstrap
|
||||
{
|
||||
private static int _initialized;
|
||||
|
||||
internal static void EnsureInitialized()
|
||||
internal static void InitPlatform()
|
||||
{
|
||||
if (System.Threading.Interlocked.Exchange(ref _initialized, 1) != 0)
|
||||
return;
|
||||
|
||||
Platform.DefaultRedirectUri = ViewModels.Settings.AndroidRedirectUri;
|
||||
Platform.CreateBrowser = () =>
|
||||
{
|
||||
var activity = MainActivity.Current;
|
||||
|
|
|
|||
|
|
@ -2,31 +2,5 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<application android:label="PostIt" android:icon="@drawable/Icon">
|
||||
|
||||
<!--
|
||||
Deep-link receiver for the OIDC Authorization Code + PKCE flow.
|
||||
After the user authenticates in the system browser, the OP
|
||||
redirects to android://postit-signin?... and Android forwards
|
||||
the Intent to the MainActivity (configured SingleTask so the
|
||||
existing instance receives OnNewIntent rather than spawning a
|
||||
new one).
|
||||
|
||||
The host value (postit-signin) MUST match the
|
||||
AndroidRedirectUri constant in PostIt/Settings/Settings.cs and
|
||||
the corresponding RedirectUri registered for the 'postit'
|
||||
client in IdentityServer (Yavsc.Org ConfigurationDb).
|
||||
-->
|
||||
<activity-alias
|
||||
android:name="PostIt.Android.OidcCallbackActivity"
|
||||
android:targetActivity="PostIt.Android.PostItMainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="android" android:host="postit-signin" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Threading.Tasks;
|
|||
using Android.App;
|
||||
using AndroidX.Browser.CustomTabs;
|
||||
using IdentityModel.OidcClient.Browser;
|
||||
using PostIt.Droid.Services;
|
||||
|
||||
namespace PostIt.Android.Services;
|
||||
|
||||
|
|
@ -35,9 +36,14 @@ public sealed class AndroidSystemBrowser : IBrowser
|
|||
};
|
||||
}
|
||||
|
||||
var uri = global::Android.Net.Uri.Parse(options.StartUrl)!;
|
||||
// 1. Enregistrez la tâche avant de lancer le Custom Tab
|
||||
var callbackTask = OidcCallbackManager.RegisterCallback(cancellationToken);
|
||||
|
||||
var callbackTask = MainActivity.AndroidOidcCallbackSink.AwaitNextCallbackAsync();
|
||||
// 2. LANCEZ VOTRE CUSTOM TAB ICI (via AndroidX.Browser.CustomTabs)
|
||||
// ... code pour ouvrir l'URL d'authentification ...
|
||||
|
||||
|
||||
var uri = global::Android.Net.Uri.Parse(options.StartUrl)!;
|
||||
|
||||
var tabsIntent = new CustomTabsIntent.Builder()
|
||||
.SetShowTitle(true)!
|
||||
|
|
|
|||
21
src/PostIt/PostIt.Android/Services/OidcCallbackManager.cs
Normal file
21
src/PostIt/PostIt.Android/Services/OidcCallbackManager.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PostIt.Droid.Services;
|
||||
|
||||
public static class OidcCallbackManager
|
||||
{
|
||||
private static TaskCompletionSource<string>? _tcs;
|
||||
|
||||
public static Task<string> RegisterCallback(CancellationToken cancellationToken)
|
||||
{
|
||||
_tcs = new TaskCompletionSource<string>();
|
||||
cancellationToken.Register(() => _tcs.TrySetCanceled());
|
||||
return _tcs.Task;
|
||||
}
|
||||
|
||||
public static void SetResult(string url)
|
||||
{
|
||||
_tcs?.TrySetResult(url);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.OS;
|
||||
using PostIt.Droid.Services;
|
||||
|
||||
namespace PostIt.Android;
|
||||
|
||||
[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop, Exported = true)]
|
||||
[IntentFilter(new[] { Intent.ActionView },
|
||||
Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
|
||||
DataScheme = "postit", // Remplacez par votre schéma personnalisé (ex: yavsc ou postit)
|
||||
DataHost = "callback")] // Correspond à postit://callback
|
||||
public class WebAuthenticationCallbackActivity : Activity
|
||||
{
|
||||
protected override void OnCreate(Bundle? savedInstanceState)
|
||||
{
|
||||
base.OnCreate(savedInstanceState);
|
||||
|
||||
// Capturer l'URL de redirection OIDC
|
||||
var url = Intent?.DataString;
|
||||
|
||||
if (!string.IsNullOrEmpty(url))
|
||||
{
|
||||
// Transmettre l'URL au gestionnaire partagé pour compléter la Task
|
||||
OidcCallbackManager.SetResult(url);
|
||||
}
|
||||
|
||||
// Fermer cette activité transparente et ramener l'application au premier plan
|
||||
var intent = new Intent(this, typeof(MainActivity));
|
||||
intent.AddFlags(ActivityFlags.ClearTop | ActivityFlags.SingleTop);
|
||||
StartActivity(intent);
|
||||
Finish();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
using PostIt.Services;
|
||||
|
||||
namespace PostIt.Desktop;
|
||||
|
||||
/// <summary>
|
||||
/// One-shot platform bootstrap. Called from <c>Program.Main</c> so that
|
||||
/// the shared OIDC login path sees a working <c>IBrowser</c> — the
|
||||
/// custom-scheme browser that hands the OIDC callback off to the
|
||||
/// running instance through the named pipe. Desktop builds do NOT use
|
||||
/// a loopback HTTP listener: the <c>postit://</c> scheme is registered
|
||||
/// with the OS at install time and the browser is whatever the user
|
||||
/// has configured to open it.
|
||||
/// </summary>
|
||||
internal static class PlatformBootstrap
|
||||
{
|
||||
private static int _initialized;
|
||||
|
||||
internal static void EnsureInitialized()
|
||||
{
|
||||
if (System.Threading.Interlocked.Exchange(ref _initialized, 1) != 0)
|
||||
return;
|
||||
|
||||
// Use the custom-scheme redirect on Desktop. Loopback is only
|
||||
// a fallback for platforms that cannot register postit://
|
||||
// (see Settings.DefaultLoopbackRedirectUri for that path).
|
||||
Platform.DefaultRedirectUri = AuthenticationSettings.DefaultDesktopRedirectUri;
|
||||
Platform.CustomScheme = "postit";
|
||||
}
|
||||
}
|
||||
|
|
@ -12,8 +12,6 @@ sealed class Program
|
|||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
PlatformBootstrap.EnsureInitialized();
|
||||
|
||||
// Short-circuit 2nd-instance launches (OS handing us the
|
||||
// postit://callback URL) BEFORE Avalonia spins up a window.
|
||||
// If we let Avalonia initialise, the new MainWindow flashes
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ public class SettingsLoadTests
|
|||
bool flip = ((workerId + i) & 1) == 0;
|
||||
settings.DarkMode = flip;
|
||||
settings.Authentication.RedirectUri =
|
||||
global::AuthenticationSettings.DefaultDesktopRedirectUri;
|
||||
global::AuthenticationSettings.DesktopRedirectUri;
|
||||
|
||||
settings.BusinessApiUrl = flip
|
||||
? "https://a.example.test/api/v1/"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
|
|
@ -73,10 +72,8 @@ public partial class App : Application
|
|||
ConfigureRootView(View);
|
||||
ApplyDarkMode(settings);
|
||||
}
|
||||
var mainVm = ServiceProvider!.GetRequiredService<HomePageViewModel>();
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
this.PushPageAsync(mainVm);
|
||||
}
|
||||
|
||||
private void ConfigureRootView(MainView rootView)
|
||||
|
|
@ -93,9 +90,9 @@ private void ConfigureRootView(MainView rootView)
|
|||
rootView.NavRoot.PopToRootAsync();
|
||||
};
|
||||
|
||||
sessionStatus.LoginSucceeded += () =>
|
||||
sessionStatus.LoginSucceeded += async () =>
|
||||
{
|
||||
PushMainPageAsync();
|
||||
await PushMainPageAsync();
|
||||
};
|
||||
|
||||
rootView.SessionBanner.DataContext = sessionStatus;
|
||||
|
|
@ -133,6 +130,9 @@ private void ConfigureRootView(MainView rootView)
|
|||
var refreshed = await api.TrySilentLoginAsync().ConfigureAwait(true);
|
||||
var sessionStatus = provider.GetRequiredService<SessionStatusViewModel>();
|
||||
sessionStatus.Refresh();
|
||||
var homePage = provider.GetRequiredService<HomePageViewModel>();
|
||||
var app = (App)Current!;
|
||||
await app.PushPageAsync(homePage);
|
||||
if (!refreshed) return;
|
||||
|
||||
await PushMainPageAsync().ConfigureAwait(true);
|
||||
|
|
|
|||
|
|
@ -21,14 +21,14 @@ public static class Platform
|
|||
/// override this property at startup (e.g. PostIt.Android sets
|
||||
/// it to <c>android://postit-signin</c>).
|
||||
/// </summary>
|
||||
public static string DefaultRedirectUri { get; set; } = "postit://callback";
|
||||
public const string RedirectUri = "postit://callback";
|
||||
|
||||
/// <summary>
|
||||
/// Scheme prefix the <see cref="CustomSchemeBrowser"/> matches
|
||||
/// against <c>BrowserOptions.EndUrl</c>. Overridable for apps
|
||||
/// that want to register their own scheme.
|
||||
/// </summary>
|
||||
public static string CustomScheme { get; set; } = "postit";
|
||||
public const string CustomScheme = "postit";
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a fresh <see cref="IBrowser"/> for the running platform.
|
||||
|
|
@ -37,4 +37,4 @@ public static class Platform
|
|||
/// </summary>
|
||||
public static System.Func<IBrowser?>? CreateBrowser { get; set; } =
|
||||
() => new CustomSchemeBrowser(CustomScheme);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ public partial class AuthenticationSettings : ObservableObject
|
|||
/// hand-off in <see cref="PostIt.Services.SingleInstance"/>
|
||||
/// (RFC 8252 §7.1). Production Desktop builds use this.
|
||||
/// </summary>
|
||||
public const string DefaultDesktopRedirectUri = "postit://callback";
|
||||
public const string DesktopRedirectUri = "postit://callback";
|
||||
|
||||
/// <summary>
|
||||
/// Redirect URI used by the Android app. The corresponding IntentFilter
|
||||
|
|
@ -34,15 +34,19 @@ public partial class AuthenticationSettings : ObservableObject
|
|||
[ObservableProperty]
|
||||
public partial string[] Scopes { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// OAuth redirect URI. Defaults to <see cref="DefaultDesktopRedirectUri"/>
|
||||
/// OAuth redirect URI. Defaults to <see cref="DesktopRedirectUri"/>
|
||||
/// (custom URI scheme) which is the right answer for desktop
|
||||
/// production builds. Mobile platforms must set this to
|
||||
/// <see cref="AndroidRedirectUri"/> before calling <c>LoginAsync</c>.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
public partial string RedirectUri { get; set; } = DefaultDesktopRedirectUri;
|
||||
public partial string RedirectUri { get; set; }
|
||||
#if ANDROID
|
||||
= AndroidRedirectUri;
|
||||
#else
|
||||
= DesktopRedirectUri;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Space-separated view of <see cref="Scopes"/>. Exists for the
|
||||
|
|
|
|||
|
|
@ -2,13 +2,11 @@ using System.Runtime.CompilerServices;
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using IdentityModel.OidcClient;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
|
||||
[assembly: InternalsVisibleTo("PostIt.Tests")]
|
||||
|
||||
|
|
@ -18,37 +16,6 @@ public partial class Settings : ViewModelBase
|
|||
{
|
||||
const string SettingsFileName = "postit-settings.json";
|
||||
|
||||
/// <summary>
|
||||
/// Redirect URI used by the Android app. The corresponding IntentFilter
|
||||
/// in <c>PostIt.Android/Properties/AndroidManifest.xml</c> must match.
|
||||
/// </summary>
|
||||
public const string AndroidRedirectUri = "android://postit-signin";
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Process-wide canonical <see cref="Settings"/> instance, wired up
|
||||
/// at application boot by <see cref="App.OnFrameworkInitializationCompleted"/>
|
||||
/// through <see cref="BindToServiceProvider"/>. The hybrid pattern:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>The static <c>Current</c> reference gives
|
||||
/// ViewModels a non-DI way to reach the same instance (and lets
|
||||
/// the framework bindings push notifications through one stable
|
||||
/// <see cref="ObservableObject"/>).</description></item>
|
||||
/// <item><description>Tests that want to exercise a clean
|
||||
/// instance still call <c>new Settings()</c>; <c>Current</c>
|
||||
/// stays null in those contexts because <see cref="BindToServiceProvider"/>
|
||||
/// is never invoked.</description></item>
|
||||
/// <item><description>Reads (<see cref="GetCurrent"/>) are
|
||||
/// thread-safe and never allocate; mutations always go through
|
||||
/// the DI-resolved singleton so two threads cannot each register
|
||||
/// a different "current" Settings.</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
private static Settings? s_current;
|
||||
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
public partial AuthenticationSettings Authentication { get; set; } = new();
|
||||
|
||||
|
|
@ -66,7 +33,7 @@ public partial class Settings : ViewModelBase
|
|||
/// setters above all funnel through here, and we flip
|
||||
/// <see cref="IsDirty"/> in lock-step. Sub-property mutations
|
||||
/// (e.g. <c>Authentication.Authority</c>) are caught by the
|
||||
/// subscription wired up in
|
||||
/// subscription wired up in
|
||||
/// below. <see cref="ApplyJson"/> disables the flag during bulk
|
||||
/// hydration so the disk load itself does not count as a user
|
||||
/// edit.
|
||||
|
|
@ -336,7 +303,7 @@ public partial class Settings : ViewModelBase
|
|||
this.Authentication.ClientId = string.IsNullOrWhiteSpace(settings.Authentication.ClientId) ?
|
||||
AuthenticationSettings.DefaultClientId : settings.Authentication.ClientId;
|
||||
this.Authentication.RedirectUri = string.IsNullOrWhiteSpace(settings.Authentication.RedirectUri) ?
|
||||
AuthenticationSettings.DefaultDesktopRedirectUri : settings.Authentication.RedirectUri;
|
||||
AuthenticationSettings.DesktopRedirectUri : settings.Authentication.RedirectUri;
|
||||
if (settings.Authentication.Scopes is null || settings.Authentication.Scopes.Length == 0)
|
||||
{
|
||||
settings.Authentication.Scopes = AuthenticationSettings.DefaultScopes;
|
||||
|
|
@ -377,7 +344,7 @@ public partial class Settings : ViewModelBase
|
|||
{
|
||||
Authority = AuthenticationSettings.DefaultAuthority,
|
||||
ClientId = AuthenticationSettings.DefaultClientId,
|
||||
RedirectUri = AuthenticationSettings.DefaultDesktopRedirectUri,
|
||||
RedirectUri = AuthenticationSettings.DesktopRedirectUri,
|
||||
Scopes = AuthenticationSettings.DefaultScopes
|
||||
};
|
||||
this.DarkMode = false;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue