* [PATCH 0/2] android: register OpenID Connect callback activity for #4281
@ 2026-08-10 5:40 Azharul Haque
2026-08-10 5:40 ` [PATCH 1/2] fix #4281: android: register OpenID Connect callback activity Azharul Haque
` (2 more replies)
0 siblings, 3 replies; 33+ messages in thread
From: Azharul Haque @ 2026-08-10 5:40 UTC (permalink / raw)
To: pve-devel; +Cc: haque
The native Flutter "Proxmox VE Companion" app never implemented OpenID
Connect / OAuth realm login (bug #4281[0]): selecting an OAuth realm
just showed username/password fields that could never work.
Android requires an app to explicitly declare, in AndroidManifest.xml,
which activity should handle a given custom URL scheme. This series
registers flutter_web_auth_2's CallbackActivity for the
com.proxmox.app.openid:// redirect scheme used by the OAuth flow, and
fixes it up to match a later rename of that scheme.
No iOS-side changes are needed: ASWebAuthenticationSession resolves
the custom-scheme redirect at runtime without a static declaration
equivalent to this manifest entry.
Depends on the companion proxmox_dart_api_client and
proxmox_login_manager series, which implement the OIDC API calls and
login-form/OAuth-flow UI respectively.
Verified end-to-end against a real PVE server with an Authentik OIDC
realm, on both Android and iOS.
[0] https://bugzilla.proxmox.com/show_bug.cgi?id=4281
Azharul Haque (2):
fix #4281: android: register OpenID Connect callback activity
fix #4281: android: match renamed OpenID callback scheme
android/app/src/main/AndroidManifest.xml | 16 ++
linux/flutter/generated_plugin_registrant.cc | 8 +
linux/flutter/generated_plugins.cmake | 2 +
pubspec.lock | 184 +++++++++++--------
4 files changed, 138 insertions(+), 72 deletions(-)
--
2.50.1 (Apple Git-155)
^ permalink raw reply [flat|nested] 33+ messages in thread* [PATCH 1/2] fix #4281: android: register OpenID Connect callback activity 2026-08-10 5:40 [PATCH 0/2] android: register OpenID Connect callback activity for #4281 Azharul Haque @ 2026-08-10 5:40 ` Azharul Haque 2026-08-10 5:40 ` [PATCH 2/2] fix #4281: android: match renamed OpenID callback scheme Azharul Haque 2026-08-10 13:59 ` [PATCH 0/2] android: register OpenID Connect callback activity for #4281 Shan Shaji 2 siblings, 0 replies; 33+ messages in thread From: Azharul Haque @ 2026-08-10 5:40 UTC (permalink / raw) To: pve-devel; +Cc: haque proxmox_login_manager's OpenID login flow (see the corresponding patch there) opens the provider's authorization URL via flutter_web_auth_2 and expects the provider's redirect back to pveauth://openid-callback to be captured by the app. On Android this requires explicitly registering flutter_web_auth_2's CallbackActivity for that scheme; add it, following the plugin's setup instructions. Also set android:taskAffinity="" on both activities as recommended for this flutter_web_auth_2 version, to avoid a stray task appearing after the callback returns control to MainActivity. No changes needed on iOS: ASWebAuthenticationSession handles the custom-scheme redirect without any Info.plist registration. linux/flutter/generated_plugin_registrant.cc, generated_plugins.cmake and pubspec.lock are regenerated as a consequence of the new transitive dependency on flutter_web_auth_2 (and, on Linux/Windows, its desktop_webview_window fallback). Signed-off-by: Azharul Haque <haque@azharul.com> --- android/app/src/main/AndroidManifest.xml | 16 ++ linux/flutter/generated_plugin_registrant.cc | 8 + linux/flutter/generated_plugins.cmake | 2 + pubspec.lock | 184 +++++++++++-------- 4 files changed, 138 insertions(+), 72 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 66135eb..884fedd 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ android:name="com.proxmox.app.pve_flutter_frontend.MainActivity" android:exported="true" android:launchMode="singleTop" + android:taskAffinity="" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" @@ -23,6 +24,21 @@ </intent-filter> </activity> + <!-- Captures the OpenID Connect provider's redirect back into the app + for realms configured with an OAuth/OIDC login, see + flutter_web_auth_2 and ProxmoxLoginForm's OpenID login flow. --> + <activity + android:name="com.linusu.flutter_web_auth_2.CallbackActivity" + android:exported="true" + android:taskAffinity=""> + <intent-filter android:label="flutter_web_auth_2"> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="pveauth" /> + </intent-filter> + </activity> + <!-- This is used by Flutter to generate GeneratedPluginRegistrant.java --> <meta-data android:name="flutterEmbedding" diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e3392af..5d804a0 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,13 +7,21 @@ #include "generated_plugin_registrant.h" #include <biometric_storage/biometric_storage_plugin.h> +#include <desktop_webview_window/desktop_webview_window_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h> +#include <window_to_front/window_to_front_plugin.h> void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) biometric_storage_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "BiometricStoragePlugin"); biometric_storage_plugin_register_with_registrar(biometric_storage_registrar); + g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin"); + desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) window_to_front_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowToFrontPlugin"); + window_to_front_plugin_register_with_registrar(window_to_front_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 5a42b12..cc58f44 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,7 +4,9 @@ list(APPEND FLUTTER_PLUGIN_LIST biometric_storage + desktop_webview_window url_launcher_linux + window_to_front ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/pubspec.lock b/pubspec.lock index def499d..0096409 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" + sha256: cd6add6f846f35fb79f3c315296703c1a24f3cfd7f4739d91a74961c1c7e9f1b url: "https://pub.dev" source: hosted - version: "93.0.0" + version: "100.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b + sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf" url: "https://pub.dev" source: hosted - version: "10.0.1" + version: "13.0.0" args: dependency: transitive description: @@ -53,34 +53,34 @@ packages: dependency: transitive description: name: build - sha256: aadd943f4f8cc946882c954c187e6115a84c98c81ad1d9c6cbf0895a8c85da9c + sha256: "45d14a0fb23e018d8287c32fc98d726ce466b231928ed9b9200f29bd3ccd39ae" url: "https://pub.dev" source: hosted - version: "4.0.5" + version: "4.0.7" build_config: dependency: transitive description: name: build_config - sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" + sha256: "94eaf6708fe64408c632ef2689ca3777b112f9421306ccf4f8c84d7c5c9f83f8" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + sha256: "79e05eaf15a48d7230b053a4363b8eaac0cc234bbd0134c3229455481f55cbc6" url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.1.5" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "521daf8d189deb79ba474e43a696b41c49fb3987818dbacf3308f1e03673a75e" + sha256: "5367e521935b102bdf1e735d2aab461e36b2edca6517662d088dd04cc39f8d16" url: "https://pub.dev" source: hosted - version: "2.13.1" + version: "2.15.1" built_collection: dependency: "direct main" description: @@ -93,18 +93,18 @@ packages: dependency: "direct main" description: name: built_value - sha256: "0730c18c770d05636a8f945c32a4d7d81cb6e0f0148c8db4ad12e7748f7e49af" + sha256: "31b24be6615ec7fcf70b3aa5a7469fe35826485e639a16dd7eb83ba30e4cc6a8" url: "https://pub.dev" source: hosted - version: "8.12.5" + version: "8.12.7" built_value_generator: dependency: "direct dev" description: name: built_value_generator - sha256: ebdc4dbc63bcdb8c63eb39569bc1da8594d998862449b8dc0e064b7b999d7c96 + sha256: "66091f1d4c07ed76b25a2834c49766b7baafb8843d5a1cfc1308059f898bf056" url: "https://pub.dev" source: hosted - version: "8.12.5" + version: "8.12.7" characters: dependency: transitive description: @@ -133,18 +133,10 @@ packages: dependency: transitive description: name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - code_builder: - dependency: transitive - description: - name: code_builder - sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 url: "https://pub.dev" source: hosted - version: "4.11.1" + version: "1.2.1" collection: dependency: "direct main" description: @@ -165,10 +157,10 @@ packages: dependency: transitive description: name: cronet_http - sha256: "8e77bc6f203e0bc9126e6a9092508a3435dbcb04da3b53ed1a358909385c5e0e" + sha256: "9da9860b409d71e4b8259e3dee631176d499dee23e7cd45a3024ebd5181997d8" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.0" crypto: dependency: "direct main" description: @@ -197,10 +189,18 @@ packages: dependency: transitive description: name: dart_style - sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2" + sha256: "59d53ef8eaed9d288ed9767618e2b31c4fa0383a127db59d5eb2e737a7638a60" + url: "https://pub.dev" + source: hosted + version: "3.1.9" + desktop_webview_window: + dependency: transitive + description: + name: desktop_webview_window + sha256: b6fdae2cbf9571879b1761c12f27facaf82e22d0bdc74d049907c2a09a432957 url: "https://pub.dev" source: hosted - version: "3.1.7" + version: "0.3.0" fake_async: dependency: transitive description: @@ -315,6 +315,22 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_auth_2: + dependency: transitive + description: + name: flutter_web_auth_2 + sha256: "8f9303471dcd96670878c9b7c0c4e14c37595b2add67465f6a868f17a5872dfc" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + flutter_web_auth_2_platform_interface: + dependency: transitive + description: + name: flutter_web_auth_2_platform_interface + sha256: ba0fbba55bffb47242025f96852ad1ffba34bc451568f56ef36e613612baffab + url: "https://pub.dev" + source: hosted + version: "5.0.0" flutter_web_plugins: dependency: transitive description: flutter @@ -348,10 +364,10 @@ packages: dependency: transitive description: name: hooks - sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "2.0.2" http: dependency: transitive description: @@ -388,10 +404,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" url: "https://pub.dev" source: hosted - version: "0.20.2" + version: "0.20.3" io: dependency: transitive description: @@ -404,18 +420,34 @@ packages: dependency: transitive description: name: jni - sha256: "8706a77e94c76fe9ec9315e18949cc9479cc03af97085ca9c1077b61323ea12d" + sha256: f038e58b4dc2c9037f50e233175086337e0b305e356d28211bf55f21c504cbd3 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "7b717011ea40d04fd47c2731d3d1d36eb99eba3435c2753d62489e8c3c9991d5" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + jni_util: + dependency: transitive + description: + name: jni_util + sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f" url: "https://pub.dev" source: hosted - version: "0.15.2" + version: "1.0.0" json_annotation: dependency: transitive description: name: json_annotation - sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" url: "https://pub.dev" source: hosted - version: "4.11.0" + version: "4.12.0" leak_tracker: dependency: transitive description: @@ -476,10 +508,10 @@ packages: dependency: "direct main" description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mime: dependency: transitive description: @@ -488,14 +520,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" - url: "https://pub.dev" - source: hosted - version: "0.17.6" nested: dependency: transitive description: @@ -508,10 +532,10 @@ packages: dependency: transitive description: name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e url: "https://pub.dev" source: hosted - version: "9.3.0" + version: "9.5.0" package_config: dependency: transitive description: @@ -532,18 +556,18 @@ packages: dependency: "direct main" description: name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.6" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba" + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" url: "https://pub.dev" source: hosted - version: "2.2.23" + version: "2.3.1" path_provider_foundation: dependency: transitive description: @@ -556,18 +580,18 @@ packages: dependency: transitive description: name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" path_provider_windows: dependency: transitive description: @@ -638,6 +662,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" retry: dependency: transitive description: @@ -666,10 +698,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 + sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7" url: "https://pub.dev" source: hosted - version: "2.4.23" + version: "2.4.27" shared_preferences_foundation: dependency: transitive description: @@ -735,10 +767,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "732792cfd197d2161a65bb029606a46e0a18ff30ef9e141a7a82172b05ea8ecd" + sha256: a603f1fb984a7391ae5978d1b92bfaaa08b350dca5c825256f925818f7943bf5 url: "https://pub.dev" source: hosted - version: "4.2.2" + version: "4.2.4" source_span: dependency: transitive description: @@ -791,10 +823,10 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" typed_data: dependency: transitive description: @@ -815,10 +847,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" + sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 url: "https://pub.dev" source: hosted - version: "6.3.29" + version: "6.3.32" url_launcher_ios: dependency: transitive description: @@ -855,10 +887,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" url_launcher_windows: dependency: transitive description: @@ -879,10 +911,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" url: "https://pub.dev" source: hosted - version: "15.0.2" + version: "15.2.0" watcher: dependency: transitive description: @@ -923,6 +955,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.15.0" + window_to_front: + dependency: transitive + description: + name: window_to_front + sha256: "14fad8984db4415e2eeb30b04bb77140b180e260d6cb66b26de126a8657a9241" + url: "https://pub.dev" + source: hosted + version: "0.0.4" xdg_directories: dependency: transitive description: @@ -940,5 +980,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.10.3 <4.0.0" - flutter: ">=3.38.4" + dart: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" -- 2.50.1 (Apple Git-155) ^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 2/2] fix #4281: android: match renamed OpenID callback scheme 2026-08-10 5:40 [PATCH 0/2] android: register OpenID Connect callback activity for #4281 Azharul Haque 2026-08-10 5:40 ` [PATCH 1/2] fix #4281: android: register OpenID Connect callback activity Azharul Haque @ 2026-08-10 5:40 ` Azharul Haque 2026-08-10 13:59 ` [PATCH 0/2] android: register OpenID Connect callback activity for #4281 Shan Shaji 2 siblings, 0 replies; 33+ messages in thread From: Azharul Haque @ 2026-08-10 5:40 UTC (permalink / raw) To: pve-devel; +Cc: haque Follow-up to the proxmox_login_manager patch renaming the callback scheme from pveauth to the namespaced com.proxmox.app.openid. Signed-off-by: Azharul Haque <haque@azharul.com> --- android/app/src/main/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 884fedd..19a9362 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -35,7 +35,7 @@ <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> - <data android:scheme="pveauth" /> + <data android:scheme="com.proxmox.app.openid" /> </intent-filter> </activity> -- 2.50.1 (Apple Git-155) ^ permalink raw reply related [flat|nested] 33+ messages in thread
* Re: [PATCH 0/2] android: register OpenID Connect callback activity for #4281 2026-08-10 5:40 [PATCH 0/2] android: register OpenID Connect callback activity for #4281 Azharul Haque 2026-08-10 5:40 ` [PATCH 1/2] fix #4281: android: register OpenID Connect callback activity Azharul Haque 2026-08-10 5:40 ` [PATCH 2/2] fix #4281: android: match renamed OpenID callback scheme Azharul Haque @ 2026-08-10 13:59 ` Shan Shaji 2026-08-10 14:47 ` [PATCH v2 0/7] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque 2 siblings, 1 reply; 33+ messages in thread From: Shan Shaji @ 2026-08-10 13:59 UTC (permalink / raw) To: Azharul Haque, pve-devel Hi Azharul, Thank you so much for your patches. If you are sending patches for the first time and haven't yet had the chance to fill out our CLA. Please check this: https://proxmox.com/en/about/open-source/developers Right now, AFAICT, you have sent series for each repository seperately: - android: register OpenID Connect callback activity for #4281 (pve_flutter_frontend) - ui: implement OpenID Connect login flow for #4281 (proxmox_login_manager) - add OpenID Connect support for #4281 (proxmox_dart_api_client) Instead of sending the changes seperately as different series. Please send them as a single series including the repository name inside the patch subject prefix [0]. It would be really helpful for us when testing and reviewing the changes. Thus reducing the noise on mailling list as well. - [0] https://pve.proxmox.com/wiki/Developer_Documentation#Preparing_Patches On Mon Aug 10, 2026 at 7:40 AM CEST, Azharul Haque wrote: > The native Flutter "Proxmox VE Companion" app never implemented OpenID > Connect / OAuth realm login (bug #4281[0]): selecting an OAuth realm > just showed username/password fields that could never work. > > Android requires an app to explicitly declare, in AndroidManifest.xml, > which activity should handle a given custom URL scheme. This series > registers flutter_web_auth_2's CallbackActivity for the > com.proxmox.app.openid:// redirect scheme used by the OAuth flow, and > fixes it up to match a later rename of that scheme. > > No iOS-side changes are needed: ASWebAuthenticationSession resolves > the custom-scheme redirect at runtime without a static declaration > equivalent to this manifest entry. > > Depends on the companion proxmox_dart_api_client and > proxmox_login_manager series, which implement the OIDC API calls and > login-form/OAuth-flow UI respectively. > > Verified end-to-end against a real PVE server with an Authentik OIDC > realm, on both Android and iOS. > > [0] https://bugzilla.proxmox.com/show_bug.cgi?id=4281 > > Azharul Haque (2): > fix #4281: android: register OpenID Connect callback activity > fix #4281: android: match renamed OpenID callback scheme > > android/app/src/main/AndroidManifest.xml | 16 ++ > linux/flutter/generated_plugin_registrant.cc | 8 + > linux/flutter/generated_plugins.cmake | 2 + > pubspec.lock | 184 +++++++++++-------- > 4 files changed, 138 insertions(+), 72 deletions(-) ^ permalink raw reply [flat|nested] 33+ messages in thread
* [PATCH v2 0/7] app: implement OpenID Connect (OAuth) realm login (#4281) 2026-08-10 13:59 ` [PATCH 0/2] android: register OpenID Connect callback activity for #4281 Shan Shaji @ 2026-08-10 14:47 ` Azharul Haque 2026-08-10 14:47 ` [PATCH dart-api-client v2 1/2] fix #4281: access: add `type` property to `PveAccessDomainModel` Azharul Haque ` (7 more replies) 0 siblings, 8 replies; 33+ messages in thread From: Azharul Haque @ 2026-08-10 14:47 UTC (permalink / raw) To: pve-devel; +Cc: haque The native Flutter "Proxmox VE Companion" app never implemented OpenID Connect / OAuth realm login (bug #4281[0]): selecting an OAuth realm just showed username/password fields that could never work. v2: resending as a single combined series across all three affected repositories, per feedback -- each patch subject is now prefixed with the (shortened) repository name it applies to. No functional changes from v1. This series spans three repositories and should be applied together: dart-api-client (2 patches): API-layer building blocks for OIDC login -- a `type` property on PveAccessDomainModel to detect OpenID realms, and helpers for PVE's /access/openid/auth-url and /access/openid/login endpoints. login-manager (3 patches): Login-form UI: hide username/password fields for OpenID realms and drive the OAuth flow via flutter_web_auth_2 (system browser / ASWebAuthenticationSession on iOS, Chrome Custom Tabs on Android -- deliberately not an in-app webview); a fix for a stale Continue button state when switching realms; and namespacing the OpenID redirect scheme under Proxmox's own reserved com.proxmox.* package prefix to avoid Android custom-URL-scheme collisions. flutter-frontend (2 patches): Android-side wiring: register flutter_web_auth_2's CallbackActivity in AndroidManifest.xml for the OpenID redirect scheme, and a follow-up fix to match a later rename of that scheme. No iOS-side changes are required here -- ASWebAuthenticationSession resolves the custom-scheme redirect at runtime without a static declaration equivalent to the Android manifest entry. Verified end-to-end against a real PVE server with an Authentik OIDC realm, on both Android and iOS. [0] https://bugzilla.proxmox.com/show_bug.cgi?id=4281 Azharul Haque (7): fix #4281: access: add `type` property to `PveAccessDomainModel` fix #4281: access: add OpenID Connect auth-url/login helpers fix #4281: ui: add OpenID Connect login flow to login form fix #4281: ui: fix stale Continue button state on realm switch fix #4281: ui: use a namespaced OpenID callback scheme fix #4281: android: register OpenID Connect callback activity fix #4281: android: match renamed OpenID callback scheme -- 2.50.1 (Apple Git-155) ^ permalink raw reply [flat|nested] 33+ messages in thread
* [PATCH dart-api-client v2 1/2] fix #4281: access: add `type` property to `PveAccessDomainModel` 2026-08-10 14:47 ` [PATCH v2 0/7] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque @ 2026-08-10 14:47 ` Azharul Haque 2026-08-20 9:17 ` Shan Shaji 2026-08-10 14:47 ` [PATCH dart-api-client v2 2/2] fix #4281: access: add OpenID Connect auth-url/login helpers Azharul Haque ` (6 subsequent siblings) 7 siblings, 1 reply; 33+ messages in thread From: Azharul Haque @ 2026-08-10 14:47 UTC (permalink / raw) To: pve-devel; +Cc: haque The realm/domain list returned by /access/domains always includes a `type` field (pam, pve, ldap, ad, openid, ...), but the model never captured it. Without it, callers have no way to tell an OpenID Connect realm apart from a password-based one, which is why the login form falls back to showing username/password fields even for realms that require a browser-based OpenID login. Add the property and an `isOpenIdRealm` convenience getter, used by the login form in the following commits. Signed-off-by: Azharul Haque <haque@azharul.com> --- lib/src/models/pve_access_domain_model.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/models/pve_access_domain_model.dart b/lib/src/models/pve_access_domain_model.dart index 6298bbe..a2fff31 100644 --- a/lib/src/models/pve_access_domain_model.dart +++ b/lib/src/models/pve_access_domain_model.dart @@ -7,12 +7,14 @@ abstract class PveAccessDomainModel implements Built<PveAccessDomainModel, PveAccessDomainModelBuilder> { // Fields String get realm; + String get type; String? get comment; String? get tfa; @BuiltValueField(wireName: 'default') int? get defaultValue; bool get isDefaultRealm => defaultValue == 1; + bool get isOpenIdRealm => type == 'openid'; PveAccessDomainModel._(); -- 2.50.1 (Apple Git-155) ^ permalink raw reply related [flat|nested] 33+ messages in thread
* Re: [PATCH dart-api-client v2 1/2] fix #4281: access: add `type` property to `PveAccessDomainModel` 2026-08-10 14:47 ` [PATCH dart-api-client v2 1/2] fix #4281: access: add `type` property to `PveAccessDomainModel` Azharul Haque @ 2026-08-20 9:17 ` Shan Shaji 2026-08-20 13:37 ` Azharul Haque 0 siblings, 1 reply; 33+ messages in thread From: Shan Shaji @ 2026-08-20 9:17 UTC (permalink / raw) To: Azharul Haque, pve-devel Hi, Thank you so much for working on this. I did a round of testing using keycloak and everything did worked well. Some comments inline. On Mon Aug 10, 2026 at 4:47 PM CEST, Azharul Haque wrote: > The realm/domain list returned by /access/domains always includes a > `type` field (pam, pve, ldap, ad, openid, ...), but the model never > captured it. Without it, callers have no way to tell an OpenID Connect > realm apart from a password-based one, which is why the login form > falls back to showing username/password fields even for realms that > require a browser-based OpenID login. > > Add the property and an `isOpenIdRealm` convenience getter, used by > the login form in the following commits. > > Signed-off-by: Azharul Haque <haque@azharul.com> > --- > lib/src/models/pve_access_domain_model.dart | 2 ++ > 1 file changed, 2 insertions(+) > > diff --git a/lib/src/models/pve_access_domain_model.dart b/lib/src/models/pve_access_domain_model.dart > index 6298bbe..a2fff31 100644 > --- a/lib/src/models/pve_access_domain_model.dart > +++ b/lib/src/models/pve_access_domain_model.dart > @@ -7,12 +7,14 @@ abstract class PveAccessDomainModel > implements Built<PveAccessDomainModel, PveAccessDomainModelBuilder> { > // Fields > String get realm; > + String get type; Instead of string type, wouldn't it be better to use an enum here as it's a fixed list of options? Like: class ProxmoxAccessDomainType extends EnumClass { static const ProxmoxAccessDomainType openid = _$openid; static const ProxmoxAccessDomainType pve = _$pve; static const ProxmoxAccessDomainType pam = _$pam; const ProxmoxAccessDomainType._(String name) : super(name); static BuiltSet<ProxmoxAccessDomainType> get values => _$pveAccessDomainTypeValues; static ProxmoxAccessDomainType valueOf(String name) => _$pveAccessDomainTypeValueOf(name); static Serializer<ProxmoxAccessDomainType> get serializer => _$pveAccessDomainTypeSerializer; } > String? get comment; > String? get tfa; > @BuiltValueField(wireName: 'default') > int? get defaultValue; > > bool get isDefaultRealm => defaultValue == 1; > + bool get isOpenIdRealm => type == 'openid'; > PveAccessDomainModel._(); > ^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [PATCH dart-api-client v2 1/2] fix #4281: access: add `type` property to `PveAccessDomainModel` 2026-08-20 9:17 ` Shan Shaji @ 2026-08-20 13:37 ` Azharul Haque 2026-08-20 14:39 ` Shan Shaji 0 siblings, 1 reply; 33+ messages in thread From: Azharul Haque @ 2026-08-20 13:37 UTC (permalink / raw) To: Shan Shaji; +Cc: pve-devel Hi Shan, Thanks for testing against Keycloak, glad to hear it worked cleanly end to end. Good call on the enum. I'll switch PveAccessDomainModel.type to a ProxmoxAccessDomainType EnumClass along the lines you sketched, and update isOpenIdRealm to compare against it. I'll fold this into a v3 alongside the other feedback (the login-manager patch split, the regex/dedup fix on the OpenID login helpers, and the pubspec/Flutter version issue you flagged) and send it tonight. Thanks again for the quick, thorough review. Best, Azharul On Thu, Aug 20, 2026 at 5:17 AM Shan Shaji <s.shaji@proxmox.com> wrote: > Hi, > > Thank you so much for working on this. I did a round of testing > using keycloak and everything did worked well. Some comments inline. > > On Mon Aug 10, 2026 at 4:47 PM CEST, Azharul Haque wrote: > > The realm/domain list returned by /access/domains always includes a > > `type` field (pam, pve, ldap, ad, openid, ...), but the model never > > captured it. Without it, callers have no way to tell an OpenID Connect > > realm apart from a password-based one, which is why the login form > > falls back to showing username/password fields even for realms that > > require a browser-based OpenID login. > > > > Add the property and an `isOpenIdRealm` convenience getter, used by > > the login form in the following commits. > > > > Signed-off-by: Azharul Haque <haque@azharul.com> > > --- > > lib/src/models/pve_access_domain_model.dart | 2 ++ > > 1 file changed, 2 insertions(+) > > > > diff --git a/lib/src/models/pve_access_domain_model.dart > b/lib/src/models/pve_access_domain_model.dart > > index 6298bbe..a2fff31 100644 > > --- a/lib/src/models/pve_access_domain_model.dart > > +++ b/lib/src/models/pve_access_domain_model.dart > > @@ -7,12 +7,14 @@ abstract class PveAccessDomainModel > > implements Built<PveAccessDomainModel, PveAccessDomainModelBuilder> > { > > // Fields > > String get realm; > > + String get type; > > Instead of string type, wouldn't it be better to use an enum here as it's > a fixed list of options? > > Like: > > class ProxmoxAccessDomainType extends EnumClass { > static const ProxmoxAccessDomainType openid = _$openid; > static const ProxmoxAccessDomainType pve = _$pve; > static const ProxmoxAccessDomainType pam = _$pam; > > const ProxmoxAccessDomainType._(String name) : super(name); > > static BuiltSet<ProxmoxAccessDomainType> get values => > _$pveAccessDomainTypeValues; > static ProxmoxAccessDomainType valueOf(String name) => > _$pveAccessDomainTypeValueOf(name); > static Serializer<ProxmoxAccessDomainType> get serializer => > _$pveAccessDomainTypeSerializer; > } > > > String? get comment; > > String? get tfa; > > @BuiltValueField(wireName: 'default') > > int? get defaultValue; > > > > bool get isDefaultRealm => defaultValue == 1; > > + bool get isOpenIdRealm => type == 'openid'; > > PveAccessDomainModel._(); > > > > > ^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [PATCH dart-api-client v2 1/2] fix #4281: access: add `type` property to `PveAccessDomainModel` 2026-08-20 13:37 ` Azharul Haque @ 2026-08-20 14:39 ` Shan Shaji 2026-08-21 2:56 ` Azharul Haque 0 siblings, 1 reply; 33+ messages in thread From: Shan Shaji @ 2026-08-20 14:39 UTC (permalink / raw) To: Azharul Haque; +Cc: pve-devel Hi, thank you very much for agreeing to incorporate my feedbacks. If possible, please also send future replies to the mailling list as well to ensure better visibility. On Thu Aug 20, 2026 at 3:37 PM CEST, Azharul Haque wrote: > Hi Shan, > > > Thanks for testing against Keycloak, glad to hear it worked cleanly end to > end. > > Good call on the enum. I'll switch PveAccessDomainModel.type to a > ProxmoxAccessDomainType EnumClass along the lines you sketched, and update > isOpenIdRealm to compare against it. I'll fold this into a v3 alongside the > other feedback (the login-manager patch split, the regex/dedup fix on the > OpenID login helpers, and the pubspec/Flutter version issue you flagged) > and send it tonight. > > > Thanks again for the quick, thorough review. > > > Best, > > Azharul > ^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [PATCH dart-api-client v2 1/2] fix #4281: access: add `type` property to `PveAccessDomainModel` 2026-08-20 14:39 ` Shan Shaji @ 2026-08-21 2:56 ` Azharul Haque 2026-08-21 8:00 ` Shan Shaji 0 siblings, 1 reply; 33+ messages in thread From: Azharul Haque @ 2026-08-21 2:56 UTC (permalink / raw) To: Shan Shaji; +Cc: pve-devel Hi Shan, I went with PveAccessDomainType to match this repo's existing convention for this kind of enum (PveNodesDiskListType, PveTaskLogStatusType, etc.). Happy to switch it to Proxmox... if you'd prefer, just let me know and I'll send a follow-up. Also, my earlier reply on this thread got held by the list for moderator approval since I'm not yet a subscribed member, so if it looked like it only reached you directly, that's why, not a delivery failure on my end. v3 with this and the rest of your feedback is on its way. Best, Azharul On Thu, Aug 20, 2026 at 10:39 AM Shan Shaji <s.shaji@proxmox.com> wrote: > Hi, thank you very much for agreeing to incorporate my feedbacks. > If possible, please also send future replies to the mailling list as > well to ensure better visibility. > > On Thu Aug 20, 2026 at 3:37 PM CEST, Azharul Haque wrote: > > Hi Shan, > > > > > > Thanks for testing against Keycloak, glad to hear it worked cleanly end > to > > end. > > > > Good call on the enum. I'll switch PveAccessDomainModel.type to a > > ProxmoxAccessDomainType EnumClass along the lines you sketched, and > update > > isOpenIdRealm to compare against it. I'll fold this into a v3 alongside > the > > other feedback (the login-manager patch split, the regex/dedup fix on the > > OpenID login helpers, and the pubspec/Flutter version issue you flagged) > > and send it tonight. > > > > > > Thanks again for the quick, thorough review. > > > > > > Best, > > > > Azharul > > > > ^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [PATCH dart-api-client v2 1/2] fix #4281: access: add `type` property to `PveAccessDomainModel` 2026-08-21 2:56 ` Azharul Haque @ 2026-08-21 8:00 ` Shan Shaji 0 siblings, 0 replies; 33+ messages in thread From: Shan Shaji @ 2026-08-21 8:00 UTC (permalink / raw) To: Azharul Haque; +Cc: pve-devel Hi Azharul, On Fri Aug 21, 2026 at 4:56 AM CEST, Azharul Haque wrote: > Hi Shan, > > > I went with PveAccessDomainType to match this repo's existing convention > for this kind of enum (PveNodesDiskListType, PveTaskLogStatusType, etc.). > Happy to switch it to Proxmox... if you'd prefer, just let me know and I'll > send a follow-up. PveAccessDomainType should be fine here. Thanks for double checking it. I used the "Proxmox.." prefix because the access domain types are common across proxmox products, also that was an example :). > Also, my earlier reply on this thread got held by the list for moderator > approval since I'm not yet a subscribed member, so if it looked like it > only reached you directly, that's why, not a delivery failure on my end. Ahh, Sorry! Somehow overlooked it. Thanks for letting me know. > > v3 with this and the rest of your feedback is on its way. > > > Best, > > Azharul > > On Thu, Aug 20, 2026 at 10:39 AM Shan Shaji <s.shaji@proxmox.com> wrote: > >> Hi, thank you very much for agreeing to incorporate my feedbacks. >> If possible, please also send future replies to the mailling list as >> well to ensure better visibility. >> >> On Thu Aug 20, 2026 at 3:37 PM CEST, Azharul Haque wrote: >> > Hi Shan, >> > >> > >> > Thanks for testing against Keycloak, glad to hear it worked cleanly end >> to >> > end. >> > >> > Good call on the enum. I'll switch PveAccessDomainModel.type to a >> > ProxmoxAccessDomainType EnumClass along the lines you sketched, and >> update >> > isOpenIdRealm to compare against it. I'll fold this into a v3 alongside >> the >> > other feedback (the login-manager patch split, the regex/dedup fix on the >> > OpenID login helpers, and the pubspec/Flutter version issue you flagged) >> > and send it tonight. >> > >> > >> > Thanks again for the quick, thorough review. >> > >> > >> > Best, >> > >> > Azharul >> > >> >> ^ permalink raw reply [flat|nested] 33+ messages in thread
* [PATCH dart-api-client v2 2/2] fix #4281: access: add OpenID Connect auth-url/login helpers 2026-08-10 14:47 ` [PATCH v2 0/7] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque 2026-08-10 14:47 ` [PATCH dart-api-client v2 1/2] fix #4281: access: add `type` property to `PveAccessDomainModel` Azharul Haque @ 2026-08-10 14:47 ` Azharul Haque 2026-08-20 9:48 ` Shan Shaji 2026-08-10 14:47 ` [PATCH login-manager v2 1/3] fix #4281: ui: add OpenID Connect login flow to login form Azharul Haque ` (5 subsequent siblings) 7 siblings, 1 reply; 33+ messages in thread From: Azharul Haque @ 2026-08-10 14:47 UTC (permalink / raw) To: pve-devel; +Cc: haque Add openIdAuthUrl() and openIdLogin(), mirroring the existing authenticate()/accessDomains() functions used by the login form before an authenticated ProxmoxApiClient exists. openIdAuthUrl() requests the provider's authorization URL for a realm from /access/openid/auth-url. openIdLogin() exchanges the state/code obtained from the provider's redirect for a PVE ticket via /access/openid/login, the same way authenticate() does for password realms. The OpenID login response carries the authenticated username in its body rather than it being known upfront by the caller, so handleOpenIdLoginResponse() is added alongside the existing handleAccessTicketResponse()/handleTfaChallengeResponse() to build Credentials from it. Signed-off-by: Azharul Haque <haque@azharul.com> --- lib/src/authenticate.dart | 85 +++++++++++++++++++++++++++++ lib/src/handle_ticket_response.dart | 36 ++++++++++++ test/test.dart | 20 +++++++ 3 files changed, 141 insertions(+) diff --git a/lib/src/authenticate.dart b/lib/src/authenticate.dart index 7bd9cef..e2d76a1 100644 --- a/lib/src/authenticate.dart +++ b/lib/src/authenticate.dart @@ -72,6 +72,91 @@ Future<ProxmoxApiClient> authenticate( } } +/// Requests the provider's authorization URL for an OpenID Connect realm. +/// +/// [redirectUrl] must match a redirect URI registered with the realm's +/// OpenID provider, and is where the provider sends the user back to after +/// they authenticate (carrying `state` and `code` query parameters). +Future<String> openIdAuthUrl( + String realm, + Uri apiBaseUrl, + Uri redirectUrl, + bool validateSSL, { + http.Client? httpClient, +}) async { + httpClient ??= getCustomIOHttpClient(validateSSL: validateSSL); + + var body = { + 'realm': realm, + 'redirect-url': redirectUrl.toString(), + }; + + try { + final path = '/api2/json/access/openid/auth-url'; + final response = await httpClient + .post(apiBaseUrl.replace(path: path), body: body) + .timeout(Duration(seconds: 25)); + + response.validate(true); + + return jsonDecode(response.body)['data'] as String; + } on NSErrorClientException catch (e) { + if (e.error.code == -1202) { + throw HandshakeException(e.message); + } + rethrow; + } on http.ClientException catch (e) { + if (e.message.contains('net::ERR_CERT_AUTHORITY_INVALID')) { + throw HandshakeException(e.message); + } + rethrow; + } +} + +/// Exchanges the `state`/`code` obtained from the OpenID provider's redirect +/// for a Proxmox VE ticket, mirroring what [authenticate] does for password +/// realms. +Future<ProxmoxApiClient> openIdLogin( + String state, + String code, + Uri apiBaseUrl, + Uri redirectUrl, + bool validateSSL, { + http.Client? httpClient, +}) async { + httpClient ??= getCustomIOHttpClient(validateSSL: validateSSL); + + var body = { + 'state': state, + 'code': code, + 'redirect-url': redirectUrl.toString(), + }; + + try { + final path = '/api2/json/access/openid/login'; + final response = await httpClient + .post(apiBaseUrl.replace(path: path), body: body) + .timeout(Duration(seconds: 25)); + + final credentials = handleOpenIdLoginResponse(response, apiBaseUrl); + + return ProxmoxApiClient( + credentials, + httpClient: httpClient, + ); + } on NSErrorClientException catch (e) { + if (e.error.code == -1202) { + throw HandshakeException(e.message); + } + rethrow; + } on http.ClientException catch (e) { + if (e.message.contains('net::ERR_CERT_AUTHORITY_INVALID')) { + throw HandshakeException(e.message); + } + rethrow; + } +} + Future<List<PveAccessDomainModel?>> accessDomains( Uri apiBaseUrl, bool validateSSL, { diff --git a/lib/src/handle_ticket_response.dart b/lib/src/handle_ticket_response.dart index ba2128f..a43aed0 100644 --- a/lib/src/handle_ticket_response.dart +++ b/lib/src/handle_ticket_response.dart @@ -39,6 +39,42 @@ Credentials handleAccessTicketResponse( ); } +Credentials handleOpenIdLoginResponse( + http.Response response, Uri apiBaseUrl) { + response.validate(false); + + final bodyJson = jsonDecode(response.body)['data']; + + final ticket = bodyJson['ticket']; + + final csrfToken = bodyJson['CSRFPreventionToken']; + + final username = bodyJson['username']; + + final ticketRegex = RegExp(r'(PVE|PMG)(?:QUAR)?:(?:(\S+):)?([A-Z0-9]{8})::') + .firstMatch(bodyJson['ticket'])!; + + final time = DateTime.fromMillisecondsSinceEpoch( + int.parse(ticketRegex.group(3)!, radix: 16) * 1000); + + TfaChallenge? tfa; + if (ticket.startsWith('PVE:!tfa!')) { + tfa = TfaChallenge.fromJson( + jsonDecode(Uri.decodeComponent(ticket.substring(9).split(':')[0]))); + } else if (bodyJson['NeedTFA'] != null && bodyJson['NeedTFA'] == 1) { + tfa = TfaChallenge.legacy(); + } + + return Credentials( + apiBaseUrl, + username, + ticket: ticket, + csrfToken: csrfToken, + expiration: time, + tfa: tfa, + ); +} + Credentials handleTfaChallengeResponse( http.Response response, Credentials pendingTfaCredentials) { response.validate(false); diff --git a/test/test.dart b/test/test.dart index 23368a2..86c2272 100644 --- a/test/test.dart +++ b/test/test.dart @@ -49,5 +49,25 @@ void main() { DateTime.fromMillisecondsSinceEpoch( int.parse('5DF8EC22', radix: 16) * 1000))); }); + + test('valid openid login response extraction', () { + final ticket = + 'PVE:jdoe@keycloak:5DF8EC22::STV4HNO1wplmsyMDM5s6SUsU4cS7sBBBw+HOCEhSSV+6WGtz3zwIzHqBhq/ziJoBs7NqqyLXG4wn9jXJCMdYht+ndqwxtdFQsUNOF1Q/eTWwcyl+Q1fmPNOIIUoxMY8OqGBVozgIimiAJxdqm+2SJnrPEmlJge6m3yf/OEVAkKFCfRMOtSuyVnIbuLx6h6obvezBUP5+ZHzeTMmmXcH4rOsOKgW9XfwryLHbkjjq9Ennx0xjQaBD9Bo5ERquY0hNmWcdPC/p7ZzILTr4xH9sJe9Na2z6GhgJyTgOCAMengyIegySMq7IKIkmsp8odF4/iIC3005/XLF4w/DjPYQUMA=='; + final csrfToken = '5DF8EDEC:/bb44xdHyVQDo2eD/8ty0WVXwMgwt1HjhVHLZX2YbxQ'; + var response = http.Response( + '{"data":{"clustername":"testcluster","username":"jdoe@keycloak","CSRFPreventionToken":"5DF8EDEC:/bb44xdHyVQDo2eD/8ty0WVXwMgwt1HjhVHLZX2YbxQ","cap":{},"ticket":"$ticket"}}', + 200); + expect( + handleOpenIdLoginResponse(response, dummyEndpoint), + isA<Credentials>() + .having((e) => e.username, 'Username', 'jdoe@keycloak') + .having((e) => e.ticket, 'Ticket', ticket) + .having((e) => e.csrfToken, 'CSRF Token', csrfToken) + .having( + (e) => e.expiration, + 'Token expiration time', + DateTime.fromMillisecondsSinceEpoch( + int.parse('5DF8EC22', radix: 16) * 1000))); + }); }); } -- 2.50.1 (Apple Git-155) ^ permalink raw reply related [flat|nested] 33+ messages in thread
* Re: [PATCH dart-api-client v2 2/2] fix #4281: access: add OpenID Connect auth-url/login helpers 2026-08-10 14:47 ` [PATCH dart-api-client v2 2/2] fix #4281: access: add OpenID Connect auth-url/login helpers Azharul Haque @ 2026-08-20 9:48 ` Shan Shaji 2026-08-20 13:41 ` Azharul Haque 0 siblings, 1 reply; 33+ messages in thread From: Shan Shaji @ 2026-08-20 9:48 UTC (permalink / raw) To: Azharul Haque, pve-devel On Mon Aug 10, 2026 at 4:47 PM CEST, Azharul Haque wrote: > Add openIdAuthUrl() and openIdLogin(), mirroring the existing > authenticate()/accessDomains() functions used by the login form > before an authenticated ProxmoxApiClient exists. > > openIdAuthUrl() requests the provider's authorization URL for a > realm from /access/openid/auth-url. openIdLogin() exchanges the > state/code obtained from the provider's redirect for a PVE ticket > via /access/openid/login, the same way authenticate() does for > password realms. > > The OpenID login response carries the authenticated username in its > body rather than it being known upfront by the caller, so > handleOpenIdLoginResponse() is added alongside the existing > handleAccessTicketResponse()/handleTfaChallengeResponse() to build > Credentials from it. > > Signed-off-by: Azharul Haque <haque@azharul.com> > --- > lib/src/authenticate.dart | 85 +++++++++++++++++++++++++++++ > lib/src/handle_ticket_response.dart | 36 ++++++++++++ > test/test.dart | 20 +++++++ > 3 files changed, 141 insertions(+) > > diff --git a/lib/src/authenticate.dart b/lib/src/authenticate.dart > index 7bd9cef..e2d76a1 100644 > --- a/lib/src/authenticate.dart > +++ b/lib/src/authenticate.dart > @@ -72,6 +72,91 @@ Future<ProxmoxApiClient> authenticate( > } > } > > +/// Requests the provider's authorization URL for an OpenID Connect realm. > +/// > +/// [redirectUrl] must match a redirect URI registered with the realm's > +/// OpenID provider, and is where the provider sends the user back to after > +/// they authenticate (carrying `state` and `code` query parameters). > +Future<String> openIdAuthUrl( > + String realm, > + Uri apiBaseUrl, > + Uri redirectUrl, > + bool validateSSL, { > + http.Client? httpClient, > +}) async { > + httpClient ??= getCustomIOHttpClient(validateSSL: validateSSL); > + > + var body = { > + 'realm': realm, > + 'redirect-url': redirectUrl.toString(), > + }; > + > + try { > + final path = '/api2/json/access/openid/auth-url'; > + final response = await httpClient > + .post(apiBaseUrl.replace(path: path), body: body) > + .timeout(Duration(seconds: 25)); > + > + response.validate(true); > + > + return jsonDecode(response.body)['data'] as String; > + } on NSErrorClientException catch (e) { > + if (e.error.code == -1202) { > + throw HandshakeException(e.message); > + } > + rethrow; > + } on http.ClientException catch (e) { > + if (e.message.contains('net::ERR_CERT_AUTHORITY_INVALID')) { > + throw HandshakeException(e.message); > + } > + rethrow; > + } > +} > + > +/// Exchanges the `state`/`code` obtained from the OpenID provider's redirect > +/// for a Proxmox VE ticket, mirroring what [authenticate] does for password > +/// realms. > +Future<ProxmoxApiClient> openIdLogin( > + String state, > + String code, > + Uri apiBaseUrl, > + Uri redirectUrl, > + bool validateSSL, { > + http.Client? httpClient, > +}) async { > + httpClient ??= getCustomIOHttpClient(validateSSL: validateSSL); > + > + var body = { > + 'state': state, > + 'code': code, > + 'redirect-url': redirectUrl.toString(), > + }; > + > + try { > + final path = '/api2/json/access/openid/login'; > + final response = await httpClient > + .post(apiBaseUrl.replace(path: path), body: body) > + .timeout(Duration(seconds: 25)); > + > + final credentials = handleOpenIdLoginResponse(response, apiBaseUrl); > + > + return ProxmoxApiClient( > + credentials, > + httpClient: httpClient, > + ); > + } on NSErrorClientException catch (e) { > + if (e.error.code == -1202) { > + throw HandshakeException(e.message); > + } > + rethrow; > + } on http.ClientException catch (e) { > + if (e.message.contains('net::ERR_CERT_AUTHORITY_INVALID')) { > + throw HandshakeException(e.message); > + } > + rethrow; > + } > +} > + > Future<List<PveAccessDomainModel?>> accessDomains( > Uri apiBaseUrl, > bool validateSSL, { > diff --git a/lib/src/handle_ticket_response.dart b/lib/src/handle_ticket_response.dart > index ba2128f..a43aed0 100644 > --- a/lib/src/handle_ticket_response.dart > +++ b/lib/src/handle_ticket_response.dart > @@ -39,6 +39,42 @@ Credentials handleAccessTicketResponse( > ); > } > > +Credentials handleOpenIdLoginResponse( > + http.Response response, Uri apiBaseUrl) { > + response.validate(false); > + final bodyJson = jsonDecode(response.body)['data']; > + > + final ticket = bodyJson['ticket']; > + > + final csrfToken = bodyJson['CSRFPreventionToken']; > + > + final username = bodyJson['username']; > + > + final ticketRegex = RegExp(r'(PVE|PMG)(?:QUAR)?:(?:(\S+):)?([A-Z0-9]{8})::') I know, you copied this regex from the handleAccessTicketResponse function. But IMHO, the first group don't need to match for PMG as well. It could just be PVE. final ticketRegex = RegExp(r'(PVE)(?:QUAR)?:(?:(\S+):)?([A-Z0-9]{8})::') > + .firstMatch(bodyJson['ticket'])!; > + > + final time = DateTime.fromMillisecondsSinceEpoch( > + int.parse(ticketRegex.group(3)!, radix: 16) * 1000); > + > + TfaChallenge? tfa; > + if (ticket.startsWith('PVE:!tfa!')) { > + tfa = TfaChallenge.fromJson( > + jsonDecode(Uri.decodeComponent(ticket.substring(9).split(':')[0]))); > + } else if (bodyJson['NeedTFA'] != null && bodyJson['NeedTFA'] == 1) { > + tfa = TfaChallenge.legacy(); > + } This block is common in both handleAccessTicketResponse and handleOpenIdLogiResponse functions. IMHO, we could seperate this into its own function. > + return Credentials( > + apiBaseUrl, > + username, > + ticket: ticket, > + csrfToken: csrfToken, > + expiration: time, > + tfa: tfa, > + ); > +} > + > Credentials handleTfaChallengeResponse( > http.Response response, Credentials pendingTfaCredentials) { > response.validate(false); > diff --git a/test/test.dart b/test/test.dart > index 23368a2..86c2272 100644 > --- a/test/test.dart > +++ b/test/test.dart > @@ -49,5 +49,25 @@ void main() { > DateTime.fromMillisecondsSinceEpoch( > int.parse('5DF8EC22', radix: 16) * 1000))); > }); > + > + test('valid openid login response extraction', () { > + final ticket = > + 'PVE:jdoe@keycloak:5DF8EC22::STV4HNO1wplmsyMDM5s6SUsU4cS7sBBBw+HOCEhSSV+6WGtz3zwIzHqBhq/ziJoBs7NqqyLXG4wn9jXJCMdYht+ndqwxtdFQsUNOF1Q/eTWwcyl+Q1fmPNOIIUoxMY8OqGBVozgIimiAJxdqm+2SJnrPEmlJge6m3yf/OEVAkKFCfRMOtSuyVnIbuLx6h6obvezBUP5+ZHzeTMmmXcH4rOsOKgW9XfwryLHbkjjq9Ennx0xjQaBD9Bo5ERquY0hNmWcdPC/p7ZzILTr4xH9sJe9Na2z6GhgJyTgOCAMengyIegySMq7IKIkmsp8odF4/iIC3005/XLF4w/DjPYQUMA=='; > + final csrfToken = '5DF8EDEC:/bb44xdHyVQDo2eD/8ty0WVXwMgwt1HjhVHLZX2YbxQ'; > + var response = http.Response( > + '{"data":{"clustername":"testcluster","username":"jdoe@keycloak","CSRFPreventionToken":"5DF8EDEC:/bb44xdHyVQDo2eD/8ty0WVXwMgwt1HjhVHLZX2YbxQ","cap":{},"ticket":"$ticket"}}', > + 200); > + expect( > + handleOpenIdLoginResponse(response, dummyEndpoint), > + isA<Credentials>() > + .having((e) => e.username, 'Username', 'jdoe@keycloak') > + .having((e) => e.ticket, 'Ticket', ticket) > + .having((e) => e.csrfToken, 'CSRF Token', csrfToken) > + .having( > + (e) => e.expiration, > + 'Token expiration time', > + DateTime.fromMillisecondsSinceEpoch( > + int.parse('5DF8EC22', radix: 16) * 1000))); > + }); > }); > } If you would like to make the changes I mentioned, please feel free to do that, else I can do it in a seperate series. ^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [PATCH dart-api-client v2 2/2] fix #4281: access: add OpenID Connect auth-url/login helpers 2026-08-20 9:48 ` Shan Shaji @ 2026-08-20 13:41 ` Azharul Haque 0 siblings, 0 replies; 33+ messages in thread From: Azharul Haque @ 2026-08-20 13:41 UTC (permalink / raw) To: Shan Shaji; +Cc: pve-devel Hi Shan, Good catches, thanks. You're right that the PMG alternative in the ticket regex doesn't belong here since OpenID login only applies to PVE, I'll narrow it to (PVE)(?:QUAR)?:.... I'll also factor the shared ticket regex/time/TFA parsing block out of handleAccessTicketResponse and handleOpenIdLoginResponse into its own helper, so it's not duplicated between the two. I'll include both in the v3 series tonight along with the rest of the feedback. Best, Azharul On Thu, Aug 20, 2026 at 5:49 AM Shan Shaji <s.shaji@proxmox.com> wrote: > On Mon Aug 10, 2026 at 4:47 PM CEST, Azharul Haque wrote: > > Add openIdAuthUrl() and openIdLogin(), mirroring the existing > > authenticate()/accessDomains() functions used by the login form > > before an authenticated ProxmoxApiClient exists. > > > > openIdAuthUrl() requests the provider's authorization URL for a > > realm from /access/openid/auth-url. openIdLogin() exchanges the > > state/code obtained from the provider's redirect for a PVE ticket > > via /access/openid/login, the same way authenticate() does for > > password realms. > > > > The OpenID login response carries the authenticated username in its > > body rather than it being known upfront by the caller, so > > handleOpenIdLoginResponse() is added alongside the existing > > handleAccessTicketResponse()/handleTfaChallengeResponse() to build > > Credentials from it. > > > > Signed-off-by: Azharul Haque <haque@azharul.com> > > --- > > lib/src/authenticate.dart | 85 +++++++++++++++++++++++++++++ > > lib/src/handle_ticket_response.dart | 36 ++++++++++++ > > test/test.dart | 20 +++++++ > > 3 files changed, 141 insertions(+) > > > > diff --git a/lib/src/authenticate.dart b/lib/src/authenticate.dart > > index 7bd9cef..e2d76a1 100644 > > --- a/lib/src/authenticate.dart > > +++ b/lib/src/authenticate.dart > > @@ -72,6 +72,91 @@ Future<ProxmoxApiClient> authenticate( > > } > > } > > > > +/// Requests the provider's authorization URL for an OpenID Connect > realm. > > +/// > > +/// [redirectUrl] must match a redirect URI registered with the realm's > > +/// OpenID provider, and is where the provider sends the user back to > after > > +/// they authenticate (carrying `state` and `code` query parameters). > > +Future<String> openIdAuthUrl( > > + String realm, > > + Uri apiBaseUrl, > > + Uri redirectUrl, > > + bool validateSSL, { > > + http.Client? httpClient, > > +}) async { > > + httpClient ??= getCustomIOHttpClient(validateSSL: validateSSL); > > + > > + var body = { > > + 'realm': realm, > > + 'redirect-url': redirectUrl.toString(), > > + }; > > + > > + try { > > + final path = '/api2/json/access/openid/auth-url'; > > + final response = await httpClient > > + .post(apiBaseUrl.replace(path: path), body: body) > > + .timeout(Duration(seconds: 25)); > > + > > + response.validate(true); > > + > > + return jsonDecode(response.body)['data'] as String; > > + } on NSErrorClientException catch (e) { > > + if (e.error.code == -1202) { > > + throw HandshakeException(e.message); > > + } > > + rethrow; > > + } on http.ClientException catch (e) { > > + if (e.message.contains('net::ERR_CERT_AUTHORITY_INVALID')) { > > + throw HandshakeException(e.message); > > + } > > + rethrow; > > + } > > +} > > + > > +/// Exchanges the `state`/`code` obtained from the OpenID provider's > redirect > > +/// for a Proxmox VE ticket, mirroring what [authenticate] does for > password > > +/// realms. > > +Future<ProxmoxApiClient> openIdLogin( > > + String state, > > + String code, > > + Uri apiBaseUrl, > > + Uri redirectUrl, > > + bool validateSSL, { > > + http.Client? httpClient, > > +}) async { > > + httpClient ??= getCustomIOHttpClient(validateSSL: validateSSL); > > + > > + var body = { > > + 'state': state, > > + 'code': code, > > + 'redirect-url': redirectUrl.toString(), > > + }; > > + > > + try { > > + final path = '/api2/json/access/openid/login'; > > + final response = await httpClient > > + .post(apiBaseUrl.replace(path: path), body: body) > > + .timeout(Duration(seconds: 25)); > > + > > + final credentials = handleOpenIdLoginResponse(response, apiBaseUrl); > > + > > + return ProxmoxApiClient( > > + credentials, > > + httpClient: httpClient, > > + ); > > + } on NSErrorClientException catch (e) { > > + if (e.error.code == -1202) { > > + throw HandshakeException(e.message); > > + } > > + rethrow; > > + } on http.ClientException catch (e) { > > + if (e.message.contains('net::ERR_CERT_AUTHORITY_INVALID')) { > > + throw HandshakeException(e.message); > > + } > > + rethrow; > > + } > > +} > > + > > Future<List<PveAccessDomainModel?>> accessDomains( > > Uri apiBaseUrl, > > bool validateSSL, { > > diff --git a/lib/src/handle_ticket_response.dart > b/lib/src/handle_ticket_response.dart > > index ba2128f..a43aed0 100644 > > --- a/lib/src/handle_ticket_response.dart > > +++ b/lib/src/handle_ticket_response.dart > > @@ -39,6 +39,42 @@ Credentials handleAccessTicketResponse( > > ); > > } > > > > +Credentials handleOpenIdLoginResponse( > > + http.Response response, Uri apiBaseUrl) { > > + response.validate(false); > > > > + final bodyJson = jsonDecode(response.body)['data']; > > + > > + final ticket = bodyJson['ticket']; > > + > > + final csrfToken = bodyJson['CSRFPreventionToken']; > > + > > + final username = bodyJson['username']; > > + > > + final ticketRegex = > RegExp(r'(PVE|PMG)(?:QUAR)?:(?:(\S+):)?([A-Z0-9]{8})::') > > I know, you copied this regex from the handleAccessTicketResponse > function. But IMHO, > the first group don't need to match for PMG as well. It could just be > PVE. > > final ticketRegex = > RegExp(r'(PVE)(?:QUAR)?:(?:(\S+):)?([A-Z0-9]{8})::') > > > + .firstMatch(bodyJson['ticket'])!; > > + > > + final time = DateTime.fromMillisecondsSinceEpoch( > > + int.parse(ticketRegex.group(3)!, radix: 16) * 1000); > > + > > + TfaChallenge? tfa; > > + if (ticket.startsWith('PVE:!tfa!')) { > > + tfa = TfaChallenge.fromJson( > > + > jsonDecode(Uri.decodeComponent(ticket.substring(9).split(':')[0]))); > > + } else if (bodyJson['NeedTFA'] != null && bodyJson['NeedTFA'] == > 1) { > > + tfa = TfaChallenge.legacy(); > > + } > > This block is common in both handleAccessTicketResponse and > handleOpenIdLogiResponse > functions. IMHO, we could seperate this into its own function. > > > + return Credentials( > > + apiBaseUrl, > > + username, > > + ticket: ticket, > > + csrfToken: csrfToken, > > + expiration: time, > > + tfa: tfa, > > + ); > > +} > > + > > Credentials handleTfaChallengeResponse( > > http.Response response, Credentials pendingTfaCredentials) { > > response.validate(false); > > diff --git a/test/test.dart b/test/test.dart > > index 23368a2..86c2272 100644 > > --- a/test/test.dart > > +++ b/test/test.dart > > @@ -49,5 +49,25 @@ void main() { > > DateTime.fromMillisecondsSinceEpoch( > > int.parse('5DF8EC22', radix: 16) * 1000))); > > }); > > + > > + test('valid openid login response extraction', () { > > + final ticket = > > + 'PVE:jdoe@keycloak > :5DF8EC22::STV4HNO1wplmsyMDM5s6SUsU4cS7sBBBw+HOCEhSSV+6WGtz3zwIzHqBhq/ziJoBs7NqqyLXG4wn9jXJCMdYht+ndqwxtdFQsUNOF1Q/eTWwcyl+Q1fmPNOIIUoxMY8OqGBVozgIimiAJxdqm+2SJnrPEmlJge6m3yf/OEVAkKFCfRMOtSuyVnIbuLx6h6obvezBUP5+ZHzeTMmmXcH4rOsOKgW9XfwryLHbkjjq9Ennx0xjQaBD9Bo5ERquY0hNmWcdPC/p7ZzILTr4xH9sJe9Na2z6GhgJyTgOCAMengyIegySMq7IKIkmsp8odF4/iIC3005/XLF4w/DjPYQUMA=='; > > + final csrfToken = > '5DF8EDEC:/bb44xdHyVQDo2eD/8ty0WVXwMgwt1HjhVHLZX2YbxQ'; > > + var response = http.Response( > > + > '{"data":{"clustername":"testcluster","username":"jdoe@keycloak > ","CSRFPreventionToken":"5DF8EDEC:/bb44xdHyVQDo2eD/8ty0WVXwMgwt1HjhVHLZX2YbxQ","cap":{},"ticket":"$ticket"}}', > > + 200); > > + expect( > > + handleOpenIdLoginResponse(response, dummyEndpoint), > > + isA<Credentials>() > > + .having((e) => e.username, 'Username', 'jdoe@keycloak') > > + .having((e) => e.ticket, 'Ticket', ticket) > > + .having((e) => e.csrfToken, 'CSRF Token', csrfToken) > > + .having( > > + (e) => e.expiration, > > + 'Token expiration time', > > + DateTime.fromMillisecondsSinceEpoch( > > + int.parse('5DF8EC22', radix: 16) * 1000))); > > + }); > > }); > > } > > If you would like to make the changes I mentioned, please feel free to do > that, else I can do it in a seperate series. > > ^ permalink raw reply [flat|nested] 33+ messages in thread
* [PATCH login-manager v2 1/3] fix #4281: ui: add OpenID Connect login flow to login form 2026-08-10 14:47 ` [PATCH v2 0/7] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque 2026-08-10 14:47 ` [PATCH dart-api-client v2 1/2] fix #4281: access: add `type` property to `PveAccessDomainModel` Azharul Haque 2026-08-10 14:47 ` [PATCH dart-api-client v2 2/2] fix #4281: access: add OpenID Connect auth-url/login helpers Azharul Haque @ 2026-08-10 14:47 ` Azharul Haque 2026-08-20 12:31 ` Shan Shaji 2026-08-10 14:47 ` [PATCH login-manager v2 2/3] fix #4281: ui: fix stale Continue button state on realm switch Azharul Haque ` (4 subsequent siblings) 7 siblings, 1 reply; 33+ messages in thread From: Azharul Haque @ 2026-08-10 14:47 UTC (permalink / raw) To: pve-devel; +Cc: haque Realms of type openid never got a distinct login flow: the form kept showing username/password fields, and submitting against an OpenID realm just sent a password login request the realm can't handle. When the selected realm is OpenID, the form now hides the username/password fields and, on Continue, drives the browser-based OpenID flow instead of the password one: - request the provider's authorization URL via openIdAuthUrl() - open it with flutter_web_auth_2, which uses the system browser / ASWebAuthenticationSession and captures the provider's redirect to a pveauth:// callback without needing an in-app webview - exchange the returned state/code for a ticket via openIdLogin() The remainder of the login sequence (TFA challenge, fetching cluster status, persisting the login) is identical between password and OpenID logins, so it's factored out of _onLoginButtonPressed() into a shared _finishLogin() used by both flows. Signed-off-by: Azharul Haque <haque@azharul.com> --- lib/proxmox_login_form.dart | 419 +++++++++++++++++++++++------------- pubspec.lock | 160 ++++++++++++-- pubspec.yaml | 1 + 3 files changed, 409 insertions(+), 171 deletions(-) diff --git a/lib/proxmox_login_form.dart b/lib/proxmox_login_form.dart index 5b9a64e..b73165c 100644 --- a/lib/proxmox_login_form.dart +++ b/lib/proxmox_login_form.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:collection/collection.dart'; import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart' as proxclient; @@ -12,6 +13,12 @@ import 'package:proxmox_login_manager/proxmox_tfa_form.dart'; import 'package:proxmox_login_manager/extension.dart'; import 'package:proxmox_login_manager/proxmox_password_store.dart'; +/// Custom URL scheme the identity provider redirects back to once an +/// OpenID Connect login completes. Must be registered as a valid redirect +/// URI with the realm's provider, as well as in the platform manifests +/// (AndroidManifest.xml / Info.plist). +const String openIdCallbackScheme = 'pveauth'; + class ProxmoxProgressModel { int inProgress = 0; String message = 'Loading...'; @@ -85,6 +92,8 @@ class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> { ); } + final isOpenId = widget.selectedDomain?.isOpenIdRealm ?? false; + return AutofillGroup( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -97,20 +106,6 @@ class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> { controller: widget.originController, enabled: false, ), - TextFormField( - decoration: const InputDecoration( - icon: Icon(Icons.person), - labelText: 'Username', - ), - controller: widget.usernameController, - validator: (value) { - if (value!.isEmpty) { - return 'Please enter username'; - } - return null; - }, - autofillHints: const [AutofillHints.username], - ), DropdownButtonFormField( decoration: const InputDecoration(icon: Icon(Icons.domain)), items: widget.accessDomains! @@ -127,54 +122,80 @@ class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> { widget.accessDomains!.map((e) => Text(e!.realm)).toList(), initialValue: widget.selectedDomain, ), - Stack( - children: [ - TextFormField( - decoration: const InputDecoration( - icon: Icon(Icons.lock), - labelText: 'Password', + if (isOpenId) + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Text( + "This realm signs you in through your browser. " + "Tap Continue to proceed.", + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + ) + else ...[ + TextFormField( + decoration: const InputDecoration( + icon: Icon(Icons.person), + labelText: 'Username', + ), + controller: widget.usernameController, + validator: (value) { + if (value!.isEmpty) { + return 'Please enter username'; + } + return null; + }, + autofillHints: const [AutofillHints.username], + ), + Stack( + children: [ + TextFormField( + decoration: const InputDecoration( + icon: Icon(Icons.lock), + labelText: 'Password', + ), + controller: widget.passwordController, + obscureText: _obscure, + autocorrect: false, + focusNode: passwordFocusNode, + validator: (value) { + if (value!.isEmpty) { + return 'Please enter password'; + } + return null; + }, + onFieldSubmitted: (value) => widget.onPasswordSubmitted!(), + autofillHints: const [AutofillHints.password], ), - controller: widget.passwordController, - obscureText: _obscure, - autocorrect: false, - focusNode: passwordFocusNode, - validator: (value) { - if (value!.isEmpty) { - return 'Please enter password'; + Align( + alignment: Alignment.bottomRight, + child: IconButton( + constraints: BoxConstraints.tight(const Size(58, 58)), + iconSize: 24, + tooltip: _obscure ? "Show password" : "Hide password", + icon: Icon( + _obscure ? Icons.visibility : Icons.visibility_off), + onPressed: () => setState(() { + _obscure = !_obscure; + }), + ), + ) + ], + ), + if (widget.canSavePassword ?? false) + CheckboxListTile( + title: const Text('Save password in biometric storage'), + value: _savePwCheckbox ?? widget.passwordSaved ?? false, + onChanged: (value) { + if (widget.onSavePasswordChanged != null) { + widget.onSavePasswordChanged!(value!); } - return null; + setState(() { + _savePwCheckbox = value!; + }); }, - onFieldSubmitted: (value) => widget.onPasswordSubmitted!(), - autofillHints: const [AutofillHints.password], - ), - Align( - alignment: Alignment.bottomRight, - child: IconButton( - constraints: BoxConstraints.tight(const Size(58, 58)), - iconSize: 24, - tooltip: _obscure ? "Show password" : "Hide password", - icon: - Icon(_obscure ? Icons.visibility : Icons.visibility_off), - onPressed: () => setState(() { - _obscure = !_obscure; - }), - ), ) - ], - ), - if (widget.canSavePassword ?? false) - CheckboxListTile( - title: const Text('Save password in biometric storage'), - value: _savePwCheckbox ?? widget.passwordSaved ?? false, - onChanged: (value) { - if (widget.onSavePasswordChanged != null) { - widget.onSavePasswordChanged!(value!); - } - setState(() { - _savePwCheckbox = value!; - }); - }, - ) + ], ], ), ); @@ -418,7 +439,13 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> { }); if (isValid) { if (snapshot.hasData) { - _onLoginButtonPressed(); + if (_selectedDomain + ?.isOpenIdRealm == + true) { + _onOpenIdLoginButtonPressed(); + } else { + _onLoginButtonPressed(); + } } else { setState(() { _accessDomains = @@ -478,98 +505,13 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> { var client = await proxclient.authenticate( '$username@$realm', password, origin, settings.sslValidation!); - if (client.credentials.tfa != null && - client.credentials.tfa!.kinds().isNotEmpty) { - if (!mounted) return; - ProxmoxApiClient? tfaclient = - await Navigator.of(context).push(MaterialPageRoute( - builder: (context) => ProxmoxTfaForm( - apiClient: client, - ), - )); - - if (tfaclient != null) { - client = tfaclient; - } else { - setState(() { - _progressModel.inProgress -= 1; - }); - return; - } - } - - final status = await client.getClusterStatus(); - final hostname = - status.singleWhereOrNull((element) => element.local ?? false)?.name; - var loginStorage = await ProxmoxLoginStorage.fromLocalStorage(); - - final savePW = enteredPassword != '' && - _savePasswordCB && - enteredPassword != savedPassword; - final deletePW = enteredPassword != '' && !savePW && !_savePasswordCB; - String? id; - - if (widget.isCreate!) { - final newLogin = ProxmoxLoginModel((b) => b - ..origin = origin - ..username = username - ..realm = realm - ..productType = ProxmoxProductType.pve - ..ticket = client.credentials.ticket - ..passwordSaved = savePW - ..hostname = hostname); - - loginStorage = loginStorage!.rebuild((b) => b..logins.add(newLogin)); - id = newLogin.identifier; - } else { - loginStorage = loginStorage!.rebuild((b) => b - ..logins.rebuildWhere( - (m) => m == widget.userModel, - (b) => b - ..ticket = client.credentials.ticket - ..passwordSaved = - savePW || (deletePW ? false : b.passwordSaved ?? false) - ..hostname = hostname)); - id = widget.userModel!.identifier; - } - - if (id != null) { - try { - if (savePW) { - await savePassword(id, enteredPassword); - } else if (deletePW) { - await deletePassword(id); - } - } catch (e) { - if (!mounted) return; - await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Password saving error'), - scrollable: true, - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Could not save or delete password.'), - ExpansionTile( - title: const Text('Details'), - children: [Text(e.toString())], - ) - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Continue')), - ], - )); - } - } - await loginStorage.saveToDisk(); - - if (mounted) { - Navigator.of(context).pop(client); - } + await _finishLogin( + client, + realm: realm!, + username: username, + enteredPassword: enteredPassword, + savedPassword: savedPassword, + ); } on proxclient.ProxmoxApiException catch (e) { print(e); if (!mounted) return; @@ -618,6 +560,181 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> { }); } + Future<void> _onOpenIdLoginButtonPressed() async { + setState(() { + _progressModel + ..inProgress += 1 + ..message = 'Connecting to identity provider...'; + }); + + try { + final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage(); + final origin = normalizeUrl(_originController.text.trim()); + final realm = _selectedDomain!.realm; + final redirectUrl = + Uri(scheme: openIdCallbackScheme, host: 'openid-callback'); + + final authUrl = await proxclient.openIdAuthUrl( + realm, origin, redirectUrl, settings.sslValidation!); + + final result = await FlutterWebAuth2.authenticate( + url: authUrl, + callbackUrlScheme: openIdCallbackScheme, + ); + + final callbackUri = Uri.parse(result); + final state = callbackUri.queryParameters['state']; + final code = callbackUri.queryParameters['code']; + if (state == null || code == null) { + throw proxclient.ProxmoxApiException( + 'Identity provider did not return an authorization code', 400); + } + + final client = await proxclient.openIdLogin( + state, code, origin, redirectUrl, settings.sslValidation!); + + final fullUsername = client.credentials.username; + final username = fullUsername.contains('@') + ? fullUsername.substring(0, fullUsername.lastIndexOf('@')) + : fullUsername; + + await _finishLogin(client, realm: realm, username: username); + } on proxclient.ProxmoxApiException catch (e) { + print(e); + if (mounted) { + showDialog( + context: context, + builder: (context) => ProxmoxApiErrorDialog( + exception: e, + ), + ); + } + } catch (e, trace) { + print(e); + print(trace); + if (mounted) { + if (e.runtimeType == HandshakeException) { + showDialog( + context: context, + builder: (context) => const ProxmoxCertificateErrorDialog(), + ); + } else { + showDialog( + context: context, + builder: (context) => ConnectionErrorDialog(exception: e), + ); + } + } + } + setState(() { + _progressModel.inProgress -= 1; + }); + } + + /// Shared tail of both the password and OpenID login flows: handles a + /// pending TFA challenge, fetches cluster status, persists the login and + /// closes the login page. Returns early (without closing the page) if the + /// user cancels a TFA challenge. + Future<void> _finishLogin( + ProxmoxApiClient client, { + required String realm, + required String username, + String enteredPassword = '', + String? savedPassword, + }) async { + if (client.credentials.tfa != null && + client.credentials.tfa!.kinds().isNotEmpty) { + if (!mounted) return; + ProxmoxApiClient? tfaclient = + await Navigator.of(context).push(MaterialPageRoute( + builder: (context) => ProxmoxTfaForm( + apiClient: client, + ), + )); + + if (tfaclient != null) { + client = tfaclient; + } else { + return; + } + } + + final status = await client.getClusterStatus(); + final hostname = + status.singleWhereOrNull((element) => element.local ?? false)?.name; + var loginStorage = await ProxmoxLoginStorage.fromLocalStorage(); + + final savePW = enteredPassword != '' && + _savePasswordCB && + enteredPassword != savedPassword; + final deletePW = enteredPassword != '' && !savePW && !_savePasswordCB; + String? id; + + final origin = normalizeUrl(_originController.text.trim()); + + if (widget.isCreate!) { + final newLogin = ProxmoxLoginModel((b) => b + ..origin = origin + ..username = username + ..realm = realm + ..productType = ProxmoxProductType.pve + ..ticket = client.credentials.ticket + ..passwordSaved = savePW + ..hostname = hostname); + + loginStorage = loginStorage!.rebuild((b) => b..logins.add(newLogin)); + id = newLogin.identifier; + } else { + loginStorage = loginStorage!.rebuild((b) => b + ..logins.rebuildWhere( + (m) => m == widget.userModel, + (b) => b + ..ticket = client.credentials.ticket + ..passwordSaved = + savePW || (deletePW ? false : b.passwordSaved ?? false) + ..hostname = hostname)); + id = widget.userModel!.identifier; + } + + if (id != null) { + try { + if (savePW) { + await savePassword(id, enteredPassword); + } else if (deletePW) { + await deletePassword(id); + } + } catch (e) { + if (!mounted) return; + await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Password saving error'), + scrollable: true, + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Could not save or delete password.'), + ExpansionTile( + title: const Text('Details'), + children: [Text(e.toString())], + ) + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Continue')), + ], + )); + } + } + await loginStorage.saveToDisk(); + + if (mounted) { + Navigator.of(context).pop(client); + } + } + Future<List<PveAccessDomainModel?>?> _loadAccessDomains(Uri uri) async { final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage(); List<PveAccessDomainModel?>? response; diff --git a/pubspec.lock b/pubspec.lock index ac4ba18..9250de9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -133,10 +133,10 @@ packages: dependency: transitive description: name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.2.1" code_builder: dependency: transitive description: @@ -193,6 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.7" + desktop_webview_window: + dependency: transitive + description: + name: desktop_webview_window + sha256: b6fdae2cbf9571879b1761c12f27facaf82e22d0bdc74d049907c2a09a432957 + url: "https://pub.dev" + source: hosted + version: "0.3.0" fake_async: dependency: transitive description: @@ -243,6 +251,22 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_auth_2: + dependency: "direct main" + description: + name: flutter_web_auth_2 + sha256: "8f9303471dcd96670878c9b7c0c4e14c37595b2add67465f6a868f17a5872dfc" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + flutter_web_auth_2_platform_interface: + dependency: transitive + description: + name: flutter_web_auth_2_platform_interface + sha256: ba0fbba55bffb47242025f96852ad1ffba34bc451568f56ef36e613612baffab + url: "https://pub.dev" + source: hosted + version: "5.0.0" flutter_web_plugins: dependency: transitive description: flutter @@ -268,10 +292,10 @@ packages: dependency: transitive description: name: hooks - sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "2.0.2" http: dependency: transitive description: @@ -388,10 +412,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mime: dependency: transitive description: @@ -400,22 +424,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" - url: "https://pub.dev" - source: hosted - version: "0.17.6" objective_c: dependency: transitive description: name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e url: "https://pub.dev" source: hosted - version: "9.3.0" + version: "9.5.0" package_config: dependency: transitive description: @@ -432,6 +448,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 + url: "https://pub.dev" + source: hosted + version: "2.1.6" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba" + url: "https://pub.dev" + source: hosted + version: "2.2.23" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" path_provider_linux: dependency: transitive description: @@ -503,6 +543,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" retry: dependency: transitive description: @@ -648,10 +696,10 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" typed_data: dependency: transitive description: @@ -660,6 +708,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 + url: "https://pub.dev" + source: hosted + version: "6.3.32" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" vector_math: dependency: transitive description: @@ -716,6 +828,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.15.0" + window_to_front: + dependency: transitive + description: + name: window_to_front + sha256: "14fad8984db4415e2eeb30b04bb77140b180e260d6cb66b26de126a8657a9241" + url: "https://pub.dev" + source: hosted + version: "0.0.4" xdg_directories: dependency: transitive description: @@ -733,5 +853,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.10.0 <4.0.0" - flutter: ">=3.35.6" + dart: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4652ced..be38e6d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: built_collection: ^5.0.0 proxmox_dart_api_client: path: ../proxmox_dart_api_client + flutter_web_auth_2: ^5.0.3 dev_dependencies: -- 2.50.1 (Apple Git-155) ^ permalink raw reply related [flat|nested] 33+ messages in thread
* Re: [PATCH login-manager v2 1/3] fix #4281: ui: add OpenID Connect login flow to login form 2026-08-10 14:47 ` [PATCH login-manager v2 1/3] fix #4281: ui: add OpenID Connect login flow to login form Azharul Haque @ 2026-08-20 12:31 ` Shan Shaji 0 siblings, 0 replies; 33+ messages in thread From: Shan Shaji @ 2026-08-20 12:31 UTC (permalink / raw) To: Azharul Haque, pve-devel Hi, one high-level note. There are too many changes in this one patch. It would be better if we could separate it. IMHO, this patch could be split into: 1. Adding the flutter_web_auth plugin. 2. Factor out the _finishLogin function. 3. Add openId specific changes (i.e. the UI related changes and adding the function call) 4. optional: Separate the password form widget. 5. Formatting changes. We normally separate formatting changes and actual code changes into separate patches; otherwise, it will be hard to identify the actual changes. Please note that the changes I suggested can also be done by me. If you don't have the time, please let me know. I could make those changes and send it as a separate series. On Mon Aug 10, 2026 at 4:47 PM CEST, Azharul Haque wrote: [snip] > > +/// Custom URL scheme the identity provider redirects back to once an > +/// OpenID Connect login completes. Must be registered as a valid redirect > +/// URI with the realm's provider, as well as in the platform manifests > +/// (AndroidManifest.xml / Info.plist). > +const String openIdCallbackScheme = 'pveauth'; > + If you are sending a v3 by seperating the actual code changes and formatting IMO, this can also be changed to use the new scheme. (PATCH login-manager 3/3) can be dropped then. > class ProxmoxProgressModel { > int inProgress = 0; > String message = 'Loading...'; > @@ -85,6 +92,8 @@ class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> { > ); > } > > + final isOpenId = widget.selectedDomain?.isOpenIdRealm ?? false; > + > return AutofillGroup( [snip] > + if (isOpenId) > + Padding( > + padding: const EdgeInsets.symmetric(vertical: 16), > + child: Text( > + "This realm signs you in through your browser. " > + "Tap Continue to proceed.", > + style: Theme.of(context).textTheme.bodyMedium, > + textAlign: TextAlign.center, > + ), > + ) > + else ...[ Instead of "if..else.." block if we use an enum here as I mentioned in the previous reply. We could use the switch expression [0] to match the different realm types. IMHO, that would be much more cleaner and the password form can itself be another widget as well. If you are seperating the widget, it should also be another patch. - [0] https://dart.dev/language/branches#switch-expressions > + TextFormField( > + decoration: const InputDecoration( > + icon: Icon(Icons.person), > + labelText: 'Username', > + ), > + controller: widget.usernameController, > + validator: (value) { > + if (value!.isEmpty) { > + return 'Please enter username'; > + } > + return null; > + }, > + autofillHints: const [AutofillHints.username], > + ), [snip] > + Future<void> _onOpenIdLoginButtonPressed() async { [snip] > + > + await _finishLogin(client, realm: realm, username: username); > + } on proxclient.ProxmoxApiException catch (e) { > + print(e); I believe you copied this block from `_onLoginButtonPressed`. However, dart suggests to avoid adding print statements [0]. Please remove it. - [0] https://dart.dev/tools/linter-rules/avoid_print > + if (mounted) { > + showDialog( > + context: context, > + builder: (context) => ProxmoxApiErrorDialog( > + exception: e, > + ), > + ); > + } > + } catch (e, trace) { > + print(e); > + print(trace); Same here as well. > + if (mounted) { > + if (e.runtimeType == HandshakeException) { > + showDialog( > + context: context, > + builder: (context) => const ProxmoxCertificateErrorDialog(), > + ); > + } else { > + showDialog( > + context: context, > + builder: (context) => ConnectionErrorDialog(exception: e), > + ); > + } > + } > + } > + setState(() { > + _progressModel.inProgress -= 1; > + }); > + } > + > + /// Shared tail of both the password and OpenID login flows: handles a > + /// pending TFA challenge, fetches cluster status, persists the login and > + /// closes the login page. Returns early (without closing the page) if the > + /// user cancels a TFA challenge. > + Future<void> _finishLogin( > + ProxmoxApiClient client, { > + required String realm, > + required String username, > + String enteredPassword = '', > + String? savedPassword, > + }) async { IMHO, factoring out this function can itself be another patch. [snip] > diff --git a/pubspec.lock b/pubspec.lock > index ac4ba18..9250de9 100644 > --- a/pubspec.lock > +++ b/pubspec.lock > @@ -133,10 +133,10 @@ packages: > dependency: transitive > description: > name: code_assets > - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" > + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 > url: "https://pub.dev" > source: hosted > - version: "1.0.0" > + version: "1.2.1" > code_builder: > dependency: transitive > description: > @@ -193,6 +193,14 @@ packages: > url: "https://pub.dev" > source: hosted > version: "3.1.7" [snip] > sdks: > - dart: ">=3.10.0 <4.0.0" > - flutter: ">=3.35.6" > + dart: ">=3.12.0 <4.0.0" > + flutter: ">=3.44.0" May I know which flutter version you are using? Looking at this change I believe you are using a different one than the one we are using (v3.41). > diff --git a/pubspec.yaml b/pubspec.yaml > index 4652ced..be38e6d 100644 > --- a/pubspec.yaml > +++ b/pubspec.yaml > @@ -16,6 +16,7 @@ dependencies: > built_collection: ^5.0.0 > proxmox_dart_api_client: > path: ../proxmox_dart_api_client > + flutter_web_auth_2: ^5.0.3 IMHO, the .lock and .yaml file changes can itself be another patch. > > dev_dependencies: ^ permalink raw reply [flat|nested] 33+ messages in thread
* [PATCH login-manager v2 2/3] fix #4281: ui: fix stale Continue button state on realm switch 2026-08-10 14:47 ` [PATCH v2 0/7] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque ` (2 preceding siblings ...) 2026-08-10 14:47 ` [PATCH login-manager v2 1/3] fix #4281: ui: add OpenID Connect login flow to login form Azharul Haque @ 2026-08-10 14:47 ` Azharul Haque 2026-08-10 14:47 ` [PATCH login-manager v2 3/3] fix #4281: ui: use a namespaced OpenID callback scheme Azharul Haque ` (3 subsequent siblings) 7 siblings, 0 replies; 33+ messages in thread From: Azharul Haque @ 2026-08-10 14:47 UTC (permalink / raw) To: pve-devel; +Cc: haque Form(onChanged: ...) revalidates against whatever fields are currently mounted at the moment a FormField's own value changes. For the realm dropdown that runs before the following rebuild adds/removes the username/password fields for the newly selected realm's type, so Continue got enabled/disabled based on the outgoing realm's field set rather than the incoming one. Most visibly, switching from an OpenID realm to a password realm left Continue enabled with both fields empty, only failing validation once actually pressed. Recompute _submitButtonEnabled explicitly in onDomainChanged instead: OpenID realms have nothing to validate, so enable it directly; other realms are revalidated in a post-frame callback once the rebuild has settled. Do the same after the initial realm auto-selection in _getAccessDomains(), in case the default realm is an OpenID one. Signed-off-by: Azharul Haque <haque@azharul.com> --- lib/proxmox_login_form.dart | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lib/proxmox_login_form.dart b/lib/proxmox_login_form.dart index b73165c..002838c 100644 --- a/lib/proxmox_login_form.dart +++ b/lib/proxmox_login_form.dart @@ -396,6 +396,31 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> { setState(() { _selectedDomain = value; }); + // The Form's onChanged callback validates + // against whatever fields are mounted at + // the moment the dropdown's own value + // changes, which runs before this rebuild + // adds/removes the username/password + // fields for the newly selected realm. + // Recompute once that rebuild has + // happened so we validate the field set + // that's actually showing. + if (value?.isOpenIdRealm == true) { + setState(() { + _submitButtonEnabled = true; + }); + } else { + WidgetsBinding.instance + .addPostFrameCallback((_) { + if (!mounted) return; + setState(() { + _submitButtonEnabled = _formKey + .currentState + ?.validate() ?? + false; + }); + }); + } }, onOriginSubmitted: () { final isValid = @@ -828,6 +853,9 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> { setState(() { _progressModel.inProgress -= 1; _selectedDomain = selection; + // An OpenID realm has no username/password to fill in, so there's + // nothing for the form to validate before Continue is usable. + _submitButtonEnabled = selection?.isOpenIdRealm == true; }); return response; -- 2.50.1 (Apple Git-155) ^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH login-manager v2 3/3] fix #4281: ui: use a namespaced OpenID callback scheme 2026-08-10 14:47 ` [PATCH v2 0/7] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque ` (3 preceding siblings ...) 2026-08-10 14:47 ` [PATCH login-manager v2 2/3] fix #4281: ui: fix stale Continue button state on realm switch Azharul Haque @ 2026-08-10 14:47 ` Azharul Haque 2026-08-20 14:28 ` Shan Shaji 2026-08-10 14:47 ` [PATCH flutter-frontend v2 1/2] fix #4281: android: register OpenID Connect callback activity Azharul Haque ` (2 subsequent siblings) 7 siblings, 1 reply; 33+ messages in thread From: Azharul Haque @ 2026-08-10 14:47 UTC (permalink / raw) To: pve-devel; +Cc: haque pveauth:// worked in testing, but on Android a custom URL scheme isn't exclusively owned the way a verified App Link is: any other app installed on the device could in principle also declare an intent-filter for the same short, guessable scheme. Use com.proxmox.app.openid instead, derived from the app's own package/bundle identifier (com.proxmox.*, reserved for Proxmox on both app stores), so collisions with another app's scheme are effectively ruled out rather than merely unlikely. Signed-off-by: Azharul Haque <haque@azharul.com> --- lib/proxmox_login_form.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/proxmox_login_form.dart b/lib/proxmox_login_form.dart index 002838c..c5ea090 100644 --- a/lib/proxmox_login_form.dart +++ b/lib/proxmox_login_form.dart @@ -17,7 +17,11 @@ import 'package:proxmox_login_manager/proxmox_password_store.dart'; /// OpenID Connect login completes. Must be registered as a valid redirect /// URI with the realm's provider, as well as in the platform manifests /// (AndroidManifest.xml / Info.plist). -const String openIdCallbackScheme = 'pveauth'; +/// +/// Derived from the app's own package/bundle identifier (`com.proxmox.*`, +/// reserved for Proxmox on both app stores) rather than an arbitrary word, +/// so it can't collide with another app's custom URL scheme. +const String openIdCallbackScheme = 'com.proxmox.app.openid'; class ProxmoxProgressModel { int inProgress = 0; -- 2.50.1 (Apple Git-155) ^ permalink raw reply related [flat|nested] 33+ messages in thread
* Re: [PATCH login-manager v2 3/3] fix #4281: ui: use a namespaced OpenID callback scheme 2026-08-10 14:47 ` [PATCH login-manager v2 3/3] fix #4281: ui: use a namespaced OpenID callback scheme Azharul Haque @ 2026-08-20 14:28 ` Shan Shaji 0 siblings, 0 replies; 33+ messages in thread From: Shan Shaji @ 2026-08-20 14:28 UTC (permalink / raw) To: Azharul Haque, pve-devel On Mon Aug 10, 2026 at 4:47 PM CEST, Azharul Haque wrote: > pveauth:// worked in testing, but on Android a custom URL scheme > isn't exclusively owned the way a verified App Link is: any other > app installed on the device could in principle also declare an > intent-filter for the same short, guessable scheme. > > Use com.proxmox.app.openid instead, derived from the app's own > package/bundle identifier (com.proxmox.*, reserved for Proxmox on > both app stores), so collisions with another app's scheme are > effectively ruled out rather than merely unlikely. > > Signed-off-by: Azharul Haque <haque@azharul.com> > --- > lib/proxmox_login_form.dart | 6 +++++- > 1 file changed, 5 insertions(+), 1 deletion(-) > > diff --git a/lib/proxmox_login_form.dart b/lib/proxmox_login_form.dart > index 002838c..c5ea090 100644 > --- a/lib/proxmox_login_form.dart > +++ b/lib/proxmox_login_form.dart > @@ -17,7 +17,11 @@ import 'package:proxmox_login_manager/proxmox_password_store.dart'; > /// OpenID Connect login completes. Must be registered as a valid redirect > /// URI with the realm's provider, as well as in the platform manifests > /// (AndroidManifest.xml / Info.plist). > -const String openIdCallbackScheme = 'pveauth'; > +/// > +/// Derived from the app's own package/bundle identifier (`com.proxmox.*`, > +/// reserved for Proxmox on both app stores) rather than an arbitrary word, > +/// so it can't collide with another app's custom URL scheme. > +const String openIdCallbackScheme = 'com.proxmox.app.openid'; Also, IMHO the scheme could just be (com.proxmox.app). I don't think it has to be specific for openid. Sorry, missed to mention this in the earlier replies. > class ProxmoxProgressModel { > int inProgress = 0; ^ permalink raw reply [flat|nested] 33+ messages in thread
* [PATCH flutter-frontend v2 1/2] fix #4281: android: register OpenID Connect callback activity 2026-08-10 14:47 ` [PATCH v2 0/7] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque ` (4 preceding siblings ...) 2026-08-10 14:47 ` [PATCH login-manager v2 3/3] fix #4281: ui: use a namespaced OpenID callback scheme Azharul Haque @ 2026-08-10 14:47 ` Azharul Haque 2026-08-20 14:01 ` Shan Shaji 2026-08-10 14:47 ` [PATCH flutter-frontend v2 2/2] fix #4281: android: match renamed OpenID callback scheme Azharul Haque 2026-08-21 3:41 ` [PATCH v3 00/10] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque 7 siblings, 1 reply; 33+ messages in thread From: Azharul Haque @ 2026-08-10 14:47 UTC (permalink / raw) To: pve-devel; +Cc: haque proxmox_login_manager's OpenID login flow (see the corresponding patch there) opens the provider's authorization URL via flutter_web_auth_2 and expects the provider's redirect back to pveauth://openid-callback to be captured by the app. On Android this requires explicitly registering flutter_web_auth_2's CallbackActivity for that scheme; add it, following the plugin's setup instructions. Also set android:taskAffinity="" on both activities as recommended for this flutter_web_auth_2 version, to avoid a stray task appearing after the callback returns control to MainActivity. No changes needed on iOS: ASWebAuthenticationSession handles the custom-scheme redirect without any Info.plist registration. linux/flutter/generated_plugin_registrant.cc, generated_plugins.cmake and pubspec.lock are regenerated as a consequence of the new transitive dependency on flutter_web_auth_2 (and, on Linux/Windows, its desktop_webview_window fallback). Signed-off-by: Azharul Haque <haque@azharul.com> --- android/app/src/main/AndroidManifest.xml | 16 ++ linux/flutter/generated_plugin_registrant.cc | 8 + linux/flutter/generated_plugins.cmake | 2 + pubspec.lock | 184 +++++++++++-------- 4 files changed, 138 insertions(+), 72 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 66135eb..884fedd 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ android:name="com.proxmox.app.pve_flutter_frontend.MainActivity" android:exported="true" android:launchMode="singleTop" + android:taskAffinity="" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" @@ -23,6 +24,21 @@ </intent-filter> </activity> + <!-- Captures the OpenID Connect provider's redirect back into the app + for realms configured with an OAuth/OIDC login, see + flutter_web_auth_2 and ProxmoxLoginForm's OpenID login flow. --> + <activity + android:name="com.linusu.flutter_web_auth_2.CallbackActivity" + android:exported="true" + android:taskAffinity=""> + <intent-filter android:label="flutter_web_auth_2"> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="pveauth" /> + </intent-filter> + </activity> + <!-- This is used by Flutter to generate GeneratedPluginRegistrant.java --> <meta-data android:name="flutterEmbedding" diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e3392af..5d804a0 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,13 +7,21 @@ #include "generated_plugin_registrant.h" #include <biometric_storage/biometric_storage_plugin.h> +#include <desktop_webview_window/desktop_webview_window_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h> +#include <window_to_front/window_to_front_plugin.h> void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) biometric_storage_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "BiometricStoragePlugin"); biometric_storage_plugin_register_with_registrar(biometric_storage_registrar); + g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin"); + desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) window_to_front_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowToFrontPlugin"); + window_to_front_plugin_register_with_registrar(window_to_front_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 5a42b12..cc58f44 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,7 +4,9 @@ list(APPEND FLUTTER_PLUGIN_LIST biometric_storage + desktop_webview_window url_launcher_linux + window_to_front ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/pubspec.lock b/pubspec.lock index def499d..0096409 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" + sha256: cd6add6f846f35fb79f3c315296703c1a24f3cfd7f4739d91a74961c1c7e9f1b url: "https://pub.dev" source: hosted - version: "93.0.0" + version: "100.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b + sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf" url: "https://pub.dev" source: hosted - version: "10.0.1" + version: "13.0.0" args: dependency: transitive description: @@ -53,34 +53,34 @@ packages: dependency: transitive description: name: build - sha256: aadd943f4f8cc946882c954c187e6115a84c98c81ad1d9c6cbf0895a8c85da9c + sha256: "45d14a0fb23e018d8287c32fc98d726ce466b231928ed9b9200f29bd3ccd39ae" url: "https://pub.dev" source: hosted - version: "4.0.5" + version: "4.0.7" build_config: dependency: transitive description: name: build_config - sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" + sha256: "94eaf6708fe64408c632ef2689ca3777b112f9421306ccf4f8c84d7c5c9f83f8" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + sha256: "79e05eaf15a48d7230b053a4363b8eaac0cc234bbd0134c3229455481f55cbc6" url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.1.5" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "521daf8d189deb79ba474e43a696b41c49fb3987818dbacf3308f1e03673a75e" + sha256: "5367e521935b102bdf1e735d2aab461e36b2edca6517662d088dd04cc39f8d16" url: "https://pub.dev" source: hosted - version: "2.13.1" + version: "2.15.1" built_collection: dependency: "direct main" description: @@ -93,18 +93,18 @@ packages: dependency: "direct main" description: name: built_value - sha256: "0730c18c770d05636a8f945c32a4d7d81cb6e0f0148c8db4ad12e7748f7e49af" + sha256: "31b24be6615ec7fcf70b3aa5a7469fe35826485e639a16dd7eb83ba30e4cc6a8" url: "https://pub.dev" source: hosted - version: "8.12.5" + version: "8.12.7" built_value_generator: dependency: "direct dev" description: name: built_value_generator - sha256: ebdc4dbc63bcdb8c63eb39569bc1da8594d998862449b8dc0e064b7b999d7c96 + sha256: "66091f1d4c07ed76b25a2834c49766b7baafb8843d5a1cfc1308059f898bf056" url: "https://pub.dev" source: hosted - version: "8.12.5" + version: "8.12.7" characters: dependency: transitive description: @@ -133,18 +133,10 @@ packages: dependency: transitive description: name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - code_builder: - dependency: transitive - description: - name: code_builder - sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 url: "https://pub.dev" source: hosted - version: "4.11.1" + version: "1.2.1" collection: dependency: "direct main" description: @@ -165,10 +157,10 @@ packages: dependency: transitive description: name: cronet_http - sha256: "8e77bc6f203e0bc9126e6a9092508a3435dbcb04da3b53ed1a358909385c5e0e" + sha256: "9da9860b409d71e4b8259e3dee631176d499dee23e7cd45a3024ebd5181997d8" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.0" crypto: dependency: "direct main" description: @@ -197,10 +189,18 @@ packages: dependency: transitive description: name: dart_style - sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2" + sha256: "59d53ef8eaed9d288ed9767618e2b31c4fa0383a127db59d5eb2e737a7638a60" + url: "https://pub.dev" + source: hosted + version: "3.1.9" + desktop_webview_window: + dependency: transitive + description: + name: desktop_webview_window + sha256: b6fdae2cbf9571879b1761c12f27facaf82e22d0bdc74d049907c2a09a432957 url: "https://pub.dev" source: hosted - version: "3.1.7" + version: "0.3.0" fake_async: dependency: transitive description: @@ -315,6 +315,22 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_auth_2: + dependency: transitive + description: + name: flutter_web_auth_2 + sha256: "8f9303471dcd96670878c9b7c0c4e14c37595b2add67465f6a868f17a5872dfc" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + flutter_web_auth_2_platform_interface: + dependency: transitive + description: + name: flutter_web_auth_2_platform_interface + sha256: ba0fbba55bffb47242025f96852ad1ffba34bc451568f56ef36e613612baffab + url: "https://pub.dev" + source: hosted + version: "5.0.0" flutter_web_plugins: dependency: transitive description: flutter @@ -348,10 +364,10 @@ packages: dependency: transitive description: name: hooks - sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "2.0.2" http: dependency: transitive description: @@ -388,10 +404,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" url: "https://pub.dev" source: hosted - version: "0.20.2" + version: "0.20.3" io: dependency: transitive description: @@ -404,18 +420,34 @@ packages: dependency: transitive description: name: jni - sha256: "8706a77e94c76fe9ec9315e18949cc9479cc03af97085ca9c1077b61323ea12d" + sha256: f038e58b4dc2c9037f50e233175086337e0b305e356d28211bf55f21c504cbd3 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "7b717011ea40d04fd47c2731d3d1d36eb99eba3435c2753d62489e8c3c9991d5" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + jni_util: + dependency: transitive + description: + name: jni_util + sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f" url: "https://pub.dev" source: hosted - version: "0.15.2" + version: "1.0.0" json_annotation: dependency: transitive description: name: json_annotation - sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" url: "https://pub.dev" source: hosted - version: "4.11.0" + version: "4.12.0" leak_tracker: dependency: transitive description: @@ -476,10 +508,10 @@ packages: dependency: "direct main" description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mime: dependency: transitive description: @@ -488,14 +520,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" - url: "https://pub.dev" - source: hosted - version: "0.17.6" nested: dependency: transitive description: @@ -508,10 +532,10 @@ packages: dependency: transitive description: name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e url: "https://pub.dev" source: hosted - version: "9.3.0" + version: "9.5.0" package_config: dependency: transitive description: @@ -532,18 +556,18 @@ packages: dependency: "direct main" description: name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.6" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba" + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" url: "https://pub.dev" source: hosted - version: "2.2.23" + version: "2.3.1" path_provider_foundation: dependency: transitive description: @@ -556,18 +580,18 @@ packages: dependency: transitive description: name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" path_provider_windows: dependency: transitive description: @@ -638,6 +662,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" retry: dependency: transitive description: @@ -666,10 +698,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 + sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7" url: "https://pub.dev" source: hosted - version: "2.4.23" + version: "2.4.27" shared_preferences_foundation: dependency: transitive description: @@ -735,10 +767,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "732792cfd197d2161a65bb029606a46e0a18ff30ef9e141a7a82172b05ea8ecd" + sha256: a603f1fb984a7391ae5978d1b92bfaaa08b350dca5c825256f925818f7943bf5 url: "https://pub.dev" source: hosted - version: "4.2.2" + version: "4.2.4" source_span: dependency: transitive description: @@ -791,10 +823,10 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" typed_data: dependency: transitive description: @@ -815,10 +847,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" + sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 url: "https://pub.dev" source: hosted - version: "6.3.29" + version: "6.3.32" url_launcher_ios: dependency: transitive description: @@ -855,10 +887,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" url_launcher_windows: dependency: transitive description: @@ -879,10 +911,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" url: "https://pub.dev" source: hosted - version: "15.0.2" + version: "15.2.0" watcher: dependency: transitive description: @@ -923,6 +955,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.15.0" + window_to_front: + dependency: transitive + description: + name: window_to_front + sha256: "14fad8984db4415e2eeb30b04bb77140b180e260d6cb66b26de126a8657a9241" + url: "https://pub.dev" + source: hosted + version: "0.0.4" xdg_directories: dependency: transitive description: @@ -940,5 +980,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.10.3 <4.0.0" - flutter: ">=3.38.4" + dart: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" -- 2.50.1 (Apple Git-155) ^ permalink raw reply related [flat|nested] 33+ messages in thread
* Re: [PATCH flutter-frontend v2 1/2] fix #4281: android: register OpenID Connect callback activity 2026-08-10 14:47 ` [PATCH flutter-frontend v2 1/2] fix #4281: android: register OpenID Connect callback activity Azharul Haque @ 2026-08-20 14:01 ` Shan Shaji 0 siblings, 0 replies; 33+ messages in thread From: Shan Shaji @ 2026-08-20 14:01 UTC (permalink / raw) To: Azharul Haque, pve-devel On Mon Aug 10, 2026 at 4:47 PM CEST, Azharul Haque wrote: > proxmox_login_manager's OpenID login flow (see the corresponding > patch there) opens the provider's authorization URL via > flutter_web_auth_2 and expects the provider's redirect back to > pveauth://openid-callback to be captured by the app. On Android this ^-- IMHO, the new scheme could be added in this patch itself. That way the PATCH flutter-frontend 2/2 can be dropped. > requires explicitly registering flutter_web_auth_2's CallbackActivity > for that scheme; add it, following the plugin's setup instructions. > Also set android:taskAffinity="" on both activities as recommended > for this flutter_web_auth_2 version, to avoid a stray task appearing > after the callback returns control to MainActivity. > > No changes needed on iOS: ASWebAuthenticationSession handles the > custom-scheme redirect without any Info.plist registration. > > linux/flutter/generated_plugin_registrant.cc, generated_plugins.cmake > and pubspec.lock are regenerated as a consequence of the new > transitive dependency on flutter_web_auth_2 (and, on Linux/Windows, > its desktop_webview_window fallback). I think it would be better to seperate the android .xml config changes and the generated changes into its own separate patches. > > Signed-off-by: Azharul Haque <haque@azharul.com> > --- > android/app/src/main/AndroidManifest.xml | 16 ++ > linux/flutter/generated_plugin_registrant.cc | 8 + > linux/flutter/generated_plugins.cmake | 2 + > pubspec.lock | 184 +++++++++++-------- > 4 files changed, 138 insertions(+), 72 deletions(-) > > diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml > index 66135eb..884fedd 100644 > --- a/android/app/src/main/AndroidManifest.xml > +++ b/android/app/src/main/AndroidManifest.xml > @@ -12,6 +12,7 @@ > android:name="com.proxmox.app.pve_flutter_frontend.MainActivity" > android:exported="true" > android:launchMode="singleTop" > + android:taskAffinity="" > android:theme="@style/LaunchTheme" > android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" > android:hardwareAccelerated="true" > @@ -23,6 +24,21 @@ > </intent-filter> > </activity> > > + <!-- Captures the OpenID Connect provider's redirect back into the app > + for realms configured with an OAuth/OIDC login, see > + flutter_web_auth_2 and ProxmoxLoginForm's OpenID login flow. --> > + <activity > + android:name="com.linusu.flutter_web_auth_2.CallbackActivity" > + android:exported="true" > + android:taskAffinity=""> > + <intent-filter android:label="flutter_web_auth_2"> > + <action android:name="android.intent.action.VIEW" /> > + <category android:name="android.intent.category.DEFAULT" /> > + <category android:name="android.intent.category.BROWSABLE" /> > + <data android:scheme="pveauth" /> This config changes can itself be another patch. > + </intent-filter> > + </activity> > + > <!-- This is used by Flutter to generate GeneratedPluginRegistrant.java --> > <meta-data > android:name="flutterEmbedding" [snip] > sdks: > - dart: ">=3.10.3 <4.0.0" > - flutter: ">=3.38.4" > + dart: ">=3.12.0 <4.0.0" > + flutter: ">=3.44.0" As I mentioned in my previous reply we are using flutter v3.41 ^ permalink raw reply [flat|nested] 33+ messages in thread
* [PATCH flutter-frontend v2 2/2] fix #4281: android: match renamed OpenID callback scheme 2026-08-10 14:47 ` [PATCH v2 0/7] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque ` (5 preceding siblings ...) 2026-08-10 14:47 ` [PATCH flutter-frontend v2 1/2] fix #4281: android: register OpenID Connect callback activity Azharul Haque @ 2026-08-10 14:47 ` Azharul Haque 2026-08-21 3:41 ` [PATCH v3 00/10] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque 7 siblings, 0 replies; 33+ messages in thread From: Azharul Haque @ 2026-08-10 14:47 UTC (permalink / raw) To: pve-devel; +Cc: haque Follow-up to the proxmox_login_manager patch renaming the callback scheme from pveauth to the namespaced com.proxmox.app.openid. Signed-off-by: Azharul Haque <haque@azharul.com> --- android/app/src/main/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 884fedd..19a9362 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -35,7 +35,7 @@ <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> - <data android:scheme="pveauth" /> + <data android:scheme="com.proxmox.app.openid" /> </intent-filter> </activity> -- 2.50.1 (Apple Git-155) ^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH v3 00/10] app: implement OpenID Connect (OAuth) realm login (#4281) 2026-08-10 14:47 ` [PATCH v2 0/7] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque ` (6 preceding siblings ...) 2026-08-10 14:47 ` [PATCH flutter-frontend v2 2/2] fix #4281: android: match renamed OpenID callback scheme Azharul Haque @ 2026-08-21 3:41 ` Azharul Haque 2026-08-21 3:41 ` [PATCH dart-api-client v3 1/2] fix #4281: access: add `type` property to `PveAccessDomainModel` Azharul Haque ` (9 more replies) 7 siblings, 10 replies; 33+ messages in thread From: Azharul Haque @ 2026-08-21 3:41 UTC (permalink / raw) To: pve-devel; +Cc: haque The native Flutter "Proxmox VE Companion" app never implemented OpenID Connect / OAuth realm login (bug #4281[0]): selecting an OAuth realm just showed username/password fields that could never work. v3: addresses review feedback on v2, still as a single combined series across the three affected repositories: - dart-api-client: PveAccessDomainModel.type is now a built_value EnumClass (PveAccessDomainType) instead of a plain String, and the OpenID ticket-parsing helper's regex/dedup issue is fixed. - login-manager: the single large "add OpenID Connect login flow" patch is split into five, dependency, _finishLogin refactor, password-form widget extraction, the OpenID feature itself, and the pre-existing Continue-button fix, and print()/print(trace) calls copied into the new OpenID error handling are removed. - flutter-frontend: the manifest change is split into three, generated-file regeneration, the taskAffinity tweak, and the CallbackActivity registration itself. - Both repos' "rename the callback scheme" follow-up patches from v2 are gone entirely: the final scheme (com.proxmox.app, further shortened per your follow-up on the login-manager v2 3/3 patch) is now introduced directly wherever it first appears. - All lockfiles/generated files are regenerated against Flutter 3.41 / Dart 3.11 to match this project's toolchain, rather than the newer local Flutter version v2 was inadvertently built with. Verified end-to-end against a real PVE server with an Authentik OIDC realm, on both Android and iOS, plus flutter analyze/test/format under Flutter 3.41 across all three repos, plus a real Android Gradle build of the modified manifest. [0] https://bugzilla.proxmox.com/show_bug.cgi?id=4281 Azharul Haque (10): fix #4281: access: add `type` property to `PveAccessDomainModel` fix #4281: access: add OpenID Connect auth-url/login helpers fix #4281: deps: add flutter_web_auth_2 dependency refactor: ui: factor out shared login tail into _finishLogin refactor: ui: split password form into its own widget fix #4281: ui: add OpenID Connect login flow to login form fix #4281: ui: fix stale Continue button state on realm switch chore: regenerate plugin registrant for flutter_web_auth_2 fix #4281: android: set taskAffinity="" on MainActivity fix #4281: android: register OpenID Connect callback activity -- 2.50.1 (Apple Git-155) ^ permalink raw reply [flat|nested] 33+ messages in thread
* [PATCH dart-api-client v3 1/2] fix #4281: access: add `type` property to `PveAccessDomainModel` 2026-08-21 3:41 ` [PATCH v3 00/10] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque @ 2026-08-21 3:41 ` Azharul Haque 2026-08-21 3:41 ` [PATCH dart-api-client v3 2/2] fix #4281: access: add OpenID Connect auth-url/login helpers Azharul Haque ` (8 subsequent siblings) 9 siblings, 0 replies; 33+ messages in thread From: Azharul Haque @ 2026-08-21 3:41 UTC (permalink / raw) To: pve-devel; +Cc: haque The realm/domain list returned by /access/domains always includes a `type` field (pam, pve, ldap, ad, openid), but the model never captured it. Without it, callers have no way to tell an OpenID Connect realm apart from a password-based one, which is why the login form falls back to showing username/password fields even for realms that require a browser-based OpenID login. Model the field as a built_value EnumClass (`PveAccessDomainType`) rather than a plain string, since PVE's realm types are a fixed set; this gives compile-time checking and avoids stringly-typed comparisons. The enum is named following this package's existing `Pve*Type` convention (`PveNodesDiskListType`, `PveTaskLogStatusType`, ...) and covers all five realm types PVE supports. Note that unlike the previous plain string, deserializing a realm list containing an unknown type now throws; that is the standard built_value EnumClass behavior and acceptable since the set of realm types is fixed. Also add an `isOpenIdRealm` convenience getter, used by the login form in the following commits. Suggested-by: Shan Shaji <s.shaji@proxmox.com> Signed-off-by: Azharul Haque <haque@azharul.com> --- lib/src/models/pve_access_domain_model.dart | 18 ++++++++++++++++++ lib/src/models/serializers.dart | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/src/models/pve_access_domain_model.dart b/lib/src/models/pve_access_domain_model.dart index 6298bbe..39e00b9 100644 --- a/lib/src/models/pve_access_domain_model.dart +++ b/lib/src/models/pve_access_domain_model.dart @@ -1,3 +1,4 @@ +import 'package:built_collection/built_collection.dart'; import 'package:built_value/built_value.dart'; import 'package:built_value/serializer.dart'; @@ -7,12 +8,14 @@ abstract class PveAccessDomainModel implements Built<PveAccessDomainModel, PveAccessDomainModelBuilder> { // Fields String get realm; + PveAccessDomainType get type; String? get comment; String? get tfa; @BuiltValueField(wireName: 'default') int? get defaultValue; bool get isDefaultRealm => defaultValue == 1; + bool get isOpenIdRealm => type == PveAccessDomainType.openid; PveAccessDomainModel._(); @@ -23,3 +26,18 @@ abstract class PveAccessDomainModel static Serializer<PveAccessDomainModel> get serializer => _$pveAccessDomainModelSerializer; } + +class PveAccessDomainType extends EnumClass { + static const PveAccessDomainType ad = _$ad; + static const PveAccessDomainType ldap = _$ldap; + static const PveAccessDomainType openid = _$openid; + static const PveAccessDomainType pam = _$pam; + static const PveAccessDomainType pve = _$pve; + + const PveAccessDomainType._(super.name); + + static BuiltSet<PveAccessDomainType> get values => _$values; + static PveAccessDomainType valueOf(String name) => _$valueOf(name); + static Serializer<PveAccessDomainType> get serializer => + _$pveAccessDomainTypeSerializer; +} diff --git a/lib/src/models/serializers.dart b/lib/src/models/serializers.dart index 0be80fc..458d105 100644 --- a/lib/src/models/serializers.dart +++ b/lib/src/models/serializers.dart @@ -39,7 +39,8 @@ part 'serializers.g.dart'; PveAccessUserModel, PveAccessGroupModel, PveAccessRoleModel, - PveAccessDomainModel + PveAccessDomainModel, + PveAccessDomainType ]) final Serializers serializers = (_$serializers.toBuilder() ..addPlugin(StandardJsonPlugin()) -- 2.50.1 (Apple Git-155) ^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH dart-api-client v3 2/2] fix #4281: access: add OpenID Connect auth-url/login helpers 2026-08-21 3:41 ` [PATCH v3 00/10] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque 2026-08-21 3:41 ` [PATCH dart-api-client v3 1/2] fix #4281: access: add `type` property to `PveAccessDomainModel` Azharul Haque @ 2026-08-21 3:41 ` Azharul Haque 2026-08-21 3:41 ` [PATCH login-manager v3 1/5] fix #4281: deps: add flutter_web_auth_2 dependency Azharul Haque ` (7 subsequent siblings) 9 siblings, 0 replies; 33+ messages in thread From: Azharul Haque @ 2026-08-21 3:41 UTC (permalink / raw) To: pve-devel; +Cc: haque Add openIdAuthUrl() and openIdLogin(), mirroring the existing authenticate()/accessDomains() functions used by the login form before an authenticated ProxmoxApiClient exists. openIdAuthUrl() requests the provider's authorization URL for a realm from /access/openid/auth-url. openIdLogin() exchanges the state/code obtained from the provider's redirect for a PVE ticket via /access/openid/login, the same way authenticate() does for password realms. The OpenID login response carries the authenticated username in its body rather than it being known upfront by the caller, so handleOpenIdLoginResponse() is added alongside the existing handleAccessTicketResponse() to build Credentials from it. Its ticket regex only matches PVE tickets (not PMG), since OpenID login is only ever performed against a PVE realm; because the two regexes therefore differ, the regex match stays in each handler while the shared tail (deriving the expiration time and detecting an accompanying TFA challenge) is factored into a common helper used by both. Suggested-by: Shan Shaji <s.shaji@proxmox.com> Signed-off-by: Azharul Haque <haque@azharul.com> --- lib/src/authenticate.dart | 85 +++++++++++++++++++++++++++++ lib/src/handle_ticket_response.dart | 70 +++++++++++++++++++----- test/test.dart | 20 +++++++ 3 files changed, 161 insertions(+), 14 deletions(-) diff --git a/lib/src/authenticate.dart b/lib/src/authenticate.dart index 7bd9cef..e2d76a1 100644 --- a/lib/src/authenticate.dart +++ b/lib/src/authenticate.dart @@ -72,6 +72,91 @@ Future<ProxmoxApiClient> authenticate( } } +/// Requests the provider's authorization URL for an OpenID Connect realm. +/// +/// [redirectUrl] must match a redirect URI registered with the realm's +/// OpenID provider, and is where the provider sends the user back to after +/// they authenticate (carrying `state` and `code` query parameters). +Future<String> openIdAuthUrl( + String realm, + Uri apiBaseUrl, + Uri redirectUrl, + bool validateSSL, { + http.Client? httpClient, +}) async { + httpClient ??= getCustomIOHttpClient(validateSSL: validateSSL); + + var body = { + 'realm': realm, + 'redirect-url': redirectUrl.toString(), + }; + + try { + final path = '/api2/json/access/openid/auth-url'; + final response = await httpClient + .post(apiBaseUrl.replace(path: path), body: body) + .timeout(Duration(seconds: 25)); + + response.validate(true); + + return jsonDecode(response.body)['data'] as String; + } on NSErrorClientException catch (e) { + if (e.error.code == -1202) { + throw HandshakeException(e.message); + } + rethrow; + } on http.ClientException catch (e) { + if (e.message.contains('net::ERR_CERT_AUTHORITY_INVALID')) { + throw HandshakeException(e.message); + } + rethrow; + } +} + +/// Exchanges the `state`/`code` obtained from the OpenID provider's redirect +/// for a Proxmox VE ticket, mirroring what [authenticate] does for password +/// realms. +Future<ProxmoxApiClient> openIdLogin( + String state, + String code, + Uri apiBaseUrl, + Uri redirectUrl, + bool validateSSL, { + http.Client? httpClient, +}) async { + httpClient ??= getCustomIOHttpClient(validateSSL: validateSSL); + + var body = { + 'state': state, + 'code': code, + 'redirect-url': redirectUrl.toString(), + }; + + try { + final path = '/api2/json/access/openid/login'; + final response = await httpClient + .post(apiBaseUrl.replace(path: path), body: body) + .timeout(Duration(seconds: 25)); + + final credentials = handleOpenIdLoginResponse(response, apiBaseUrl); + + return ProxmoxApiClient( + credentials, + httpClient: httpClient, + ); + } on NSErrorClientException catch (e) { + if (e.error.code == -1202) { + throw HandshakeException(e.message); + } + rethrow; + } on http.ClientException catch (e) { + if (e.message.contains('net::ERR_CERT_AUTHORITY_INVALID')) { + throw HandshakeException(e.message); + } + rethrow; + } +} + Future<List<PveAccessDomainModel?>> accessDomains( Uri apiBaseUrl, bool validateSSL, { diff --git a/lib/src/handle_ticket_response.dart b/lib/src/handle_ticket_response.dart index ba2128f..f4bffde 100644 --- a/lib/src/handle_ticket_response.dart +++ b/lib/src/handle_ticket_response.dart @@ -5,6 +5,29 @@ import 'package:proxmox_dart_api_client/src/credentials.dart'; import 'package:proxmox_dart_api_client/src/extentions.dart'; import 'package:proxmox_dart_api_client/src/tfa_challenge.dart'; +/// Shared tail of parsing a ticket response: derives the ticket's +/// expiration time from its embedded timestamp, and determines whether a +/// TFA challenge accompanies it (either encoded in the ticket itself, or +/// flagged via the legacy `NeedTFA` field). +({DateTime expiration, TfaChallenge? tfa}) _parseTicketExpirationAndTfa( + RegExpMatch ticketMatch, + String ticket, + Map<String, dynamic> bodyJson, +) { + final expiration = DateTime.fromMillisecondsSinceEpoch( + int.parse(ticketMatch.group(3)!, radix: 16) * 1000); + + TfaChallenge? tfa; + if (ticket.startsWith('PVE:!tfa!')) { + tfa = TfaChallenge.fromJson( + jsonDecode(Uri.decodeComponent(ticket.substring(9).split(':')[0]))); + } else if (bodyJson['NeedTFA'] != null && bodyJson['NeedTFA'] == 1) { + tfa = TfaChallenge.legacy(); + } + + return (expiration: expiration, tfa: tfa); +} + Credentials handleAccessTicketResponse( http.Response response, Credentials unauthenticatedCredentials) { response.validate(false); @@ -15,27 +38,46 @@ Credentials handleAccessTicketResponse( final csrfToken = bodyJson['CSRFPreventionToken']; - final ticketRegex = RegExp(r'(PVE|PMG)(?:QUAR)?:(?:(\S+):)?([A-Z0-9]{8})::') - .firstMatch(bodyJson['ticket'])!; + final ticketMatch = RegExp(r'(PVE|PMG)(?:QUAR)?:(?:(\S+):)?([A-Z0-9]{8})::') + .firstMatch(ticket)!; - final time = DateTime.fromMillisecondsSinceEpoch( - int.parse(ticketRegex.group(3)!, radix: 16) * 1000); - - TfaChallenge? tfa; - if (ticket.startsWith('PVE:!tfa!')) { - tfa = TfaChallenge.fromJson( - jsonDecode(Uri.decodeComponent(ticket.substring(9).split(':')[0]))); - } else if (bodyJson['NeedTFA'] != null && bodyJson['NeedTFA'] == 1) { - tfa = TfaChallenge.legacy(); - } + final parsed = _parseTicketExpirationAndTfa(ticketMatch, ticket, bodyJson); return Credentials( unauthenticatedCredentials.apiBaseUrl, unauthenticatedCredentials.username, ticket: ticket, csrfToken: csrfToken, - expiration: time, - tfa: tfa, + expiration: parsed.expiration, + tfa: parsed.tfa, + ); +} + +Credentials handleOpenIdLoginResponse(http.Response response, Uri apiBaseUrl) { + response.validate(false); + + final bodyJson = jsonDecode(response.body)['data']; + + final ticket = bodyJson['ticket']; + + final csrfToken = bodyJson['CSRFPreventionToken']; + + final username = bodyJson['username']; + + // OpenID Connect login is only ever performed against a PVE realm, so + // unlike handleAccessTicketResponse's regex, PMG never applies here. + final ticketMatch = + RegExp(r'(PVE)(?:QUAR)?:(?:(\S+):)?([A-Z0-9]{8})::').firstMatch(ticket)!; + + final parsed = _parseTicketExpirationAndTfa(ticketMatch, ticket, bodyJson); + + return Credentials( + apiBaseUrl, + username, + ticket: ticket, + csrfToken: csrfToken, + expiration: parsed.expiration, + tfa: parsed.tfa, ); } diff --git a/test/test.dart b/test/test.dart index 23368a2..86c2272 100644 --- a/test/test.dart +++ b/test/test.dart @@ -49,5 +49,25 @@ void main() { DateTime.fromMillisecondsSinceEpoch( int.parse('5DF8EC22', radix: 16) * 1000))); }); + + test('valid openid login response extraction', () { + final ticket = + 'PVE:jdoe@keycloak:5DF8EC22::STV4HNO1wplmsyMDM5s6SUsU4cS7sBBBw+HOCEhSSV+6WGtz3zwIzHqBhq/ziJoBs7NqqyLXG4wn9jXJCMdYht+ndqwxtdFQsUNOF1Q/eTWwcyl+Q1fmPNOIIUoxMY8OqGBVozgIimiAJxdqm+2SJnrPEmlJge6m3yf/OEVAkKFCfRMOtSuyVnIbuLx6h6obvezBUP5+ZHzeTMmmXcH4rOsOKgW9XfwryLHbkjjq9Ennx0xjQaBD9Bo5ERquY0hNmWcdPC/p7ZzILTr4xH9sJe9Na2z6GhgJyTgOCAMengyIegySMq7IKIkmsp8odF4/iIC3005/XLF4w/DjPYQUMA=='; + final csrfToken = '5DF8EDEC:/bb44xdHyVQDo2eD/8ty0WVXwMgwt1HjhVHLZX2YbxQ'; + var response = http.Response( + '{"data":{"clustername":"testcluster","username":"jdoe@keycloak","CSRFPreventionToken":"5DF8EDEC:/bb44xdHyVQDo2eD/8ty0WVXwMgwt1HjhVHLZX2YbxQ","cap":{},"ticket":"$ticket"}}', + 200); + expect( + handleOpenIdLoginResponse(response, dummyEndpoint), + isA<Credentials>() + .having((e) => e.username, 'Username', 'jdoe@keycloak') + .having((e) => e.ticket, 'Ticket', ticket) + .having((e) => e.csrfToken, 'CSRF Token', csrfToken) + .having( + (e) => e.expiration, + 'Token expiration time', + DateTime.fromMillisecondsSinceEpoch( + int.parse('5DF8EC22', radix: 16) * 1000))); + }); }); } -- 2.50.1 (Apple Git-155) ^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH login-manager v3 1/5] fix #4281: deps: add flutter_web_auth_2 dependency 2026-08-21 3:41 ` [PATCH v3 00/10] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque 2026-08-21 3:41 ` [PATCH dart-api-client v3 1/2] fix #4281: access: add `type` property to `PveAccessDomainModel` Azharul Haque 2026-08-21 3:41 ` [PATCH dart-api-client v3 2/2] fix #4281: access: add OpenID Connect auth-url/login helpers Azharul Haque @ 2026-08-21 3:41 ` Azharul Haque 2026-08-21 3:41 ` [PATCH login-manager v3 2/5] refactor: ui: factor out shared login tail into _finishLogin Azharul Haque ` (6 subsequent siblings) 9 siblings, 0 replies; 33+ messages in thread From: Azharul Haque @ 2026-08-21 3:41 UTC (permalink / raw) To: pve-devel; +Cc: haque The plugin drives browser-based authentication flows via the platform- native mechanisms (ASWebAuthenticationSession on iOS, Chrome Custom Tabs on Android) rather than an embedded webview, and is used by the following commits to implement OpenID Connect realm login. The lockfile is resolved against Flutter 3.41 / Dart 3.11, matching the toolchain currently used for this project. Signed-off-by: Azharul Haque <haque@azharul.com> --- pubspec.lock | 152 +++++++++++++++++++++++++++++++++++++++++++++------ pubspec.yaml | 1 + 2 files changed, 137 insertions(+), 16 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index ac4ba18..9460221 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -133,10 +133,10 @@ packages: dependency: transitive description: name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.2.1" code_builder: dependency: transitive description: @@ -193,6 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.7" + desktop_webview_window: + dependency: transitive + description: + name: desktop_webview_window + sha256: b6fdae2cbf9571879b1761c12f27facaf82e22d0bdc74d049907c2a09a432957 + url: "https://pub.dev" + source: hosted + version: "0.3.0" fake_async: dependency: transitive description: @@ -243,6 +251,22 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_auth_2: + dependency: "direct main" + description: + name: flutter_web_auth_2 + sha256: a7655829251ee63aae64a748f8512f36670d8eba4657b0055cdf5ef304a9d164 + url: "https://pub.dev" + source: hosted + version: "5.1.0" + flutter_web_auth_2_platform_interface: + dependency: transitive + description: + name: flutter_web_auth_2_platform_interface + sha256: ba0fbba55bffb47242025f96852ad1ffba34bc451568f56ef36e613612baffab + url: "https://pub.dev" + source: hosted + version: "5.0.0" flutter_web_plugins: dependency: transitive description: flutter @@ -268,10 +292,10 @@ packages: dependency: transitive description: name: hooks - sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "2.0.2" http: dependency: transitive description: @@ -400,22 +424,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" - url: "https://pub.dev" - source: hosted - version: "0.17.6" objective_c: dependency: transitive description: name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e url: "https://pub.dev" source: hosted - version: "9.3.0" + version: "9.5.0" package_config: dependency: transitive description: @@ -432,6 +448,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 + url: "https://pub.dev" + source: hosted + version: "2.1.6" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba" + url: "https://pub.dev" + source: hosted + version: "2.2.23" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" path_provider_linux: dependency: transitive description: @@ -503,6 +543,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" retry: dependency: transitive description: @@ -660,6 +708,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c" + url: "https://pub.dev" + source: hosted + version: "6.3.30" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" vector_math: dependency: transitive description: @@ -716,6 +828,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.15.0" + window_to_front: + dependency: transitive + description: + name: window_to_front + sha256: "14fad8984db4415e2eeb30b04bb77140b180e260d6cb66b26de126a8657a9241" + url: "https://pub.dev" + source: hosted + version: "0.0.4" xdg_directories: dependency: transitive description: @@ -733,5 +853,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.10.0 <4.0.0" - flutter: ">=3.35.6" + dart: ">=3.10.3 <4.0.0" + flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index 4652ced..be38e6d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: built_collection: ^5.0.0 proxmox_dart_api_client: path: ../proxmox_dart_api_client + flutter_web_auth_2: ^5.0.3 dev_dependencies: -- 2.50.1 (Apple Git-155) ^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH login-manager v3 2/5] refactor: ui: factor out shared login tail into _finishLogin 2026-08-21 3:41 ` [PATCH v3 00/10] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque ` (2 preceding siblings ...) 2026-08-21 3:41 ` [PATCH login-manager v3 1/5] fix #4281: deps: add flutter_web_auth_2 dependency Azharul Haque @ 2026-08-21 3:41 ` Azharul Haque 2026-08-21 3:41 ` [PATCH login-manager v3 3/5] refactor: ui: split password form into its own widget Azharul Haque ` (5 subsequent siblings) 9 siblings, 0 replies; 33+ messages in thread From: Azharul Haque @ 2026-08-21 3:41 UTC (permalink / raw) To: pve-devel; +Cc: haque Move the code that runs after an authenticated API client exists -- handling a pending TFA challenge, fetching cluster status, persisting the login and closing the login page -- out of _onLoginButtonPressed into a _finishLogin method. No functional change. This prepares for the OpenID Connect login flow added in a following commit, which obtains its client through a browser-based flow instead of a password but finishes the login the same way. Suggested-by: Shan Shaji <s.shaji@proxmox.com> Signed-off-by: Azharul Haque <haque@azharul.com> --- lib/proxmox_login_form.dart | 203 ++++++++++++++++++++---------------- 1 file changed, 111 insertions(+), 92 deletions(-) diff --git a/lib/proxmox_login_form.dart b/lib/proxmox_login_form.dart index 5b9a64e..6039407 100644 --- a/lib/proxmox_login_form.dart +++ b/lib/proxmox_login_form.dart @@ -478,98 +478,13 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> { var client = await proxclient.authenticate( '$username@$realm', password, origin, settings.sslValidation!); - if (client.credentials.tfa != null && - client.credentials.tfa!.kinds().isNotEmpty) { - if (!mounted) return; - ProxmoxApiClient? tfaclient = - await Navigator.of(context).push(MaterialPageRoute( - builder: (context) => ProxmoxTfaForm( - apiClient: client, - ), - )); - - if (tfaclient != null) { - client = tfaclient; - } else { - setState(() { - _progressModel.inProgress -= 1; - }); - return; - } - } - - final status = await client.getClusterStatus(); - final hostname = - status.singleWhereOrNull((element) => element.local ?? false)?.name; - var loginStorage = await ProxmoxLoginStorage.fromLocalStorage(); - - final savePW = enteredPassword != '' && - _savePasswordCB && - enteredPassword != savedPassword; - final deletePW = enteredPassword != '' && !savePW && !_savePasswordCB; - String? id; - - if (widget.isCreate!) { - final newLogin = ProxmoxLoginModel((b) => b - ..origin = origin - ..username = username - ..realm = realm - ..productType = ProxmoxProductType.pve - ..ticket = client.credentials.ticket - ..passwordSaved = savePW - ..hostname = hostname); - - loginStorage = loginStorage!.rebuild((b) => b..logins.add(newLogin)); - id = newLogin.identifier; - } else { - loginStorage = loginStorage!.rebuild((b) => b - ..logins.rebuildWhere( - (m) => m == widget.userModel, - (b) => b - ..ticket = client.credentials.ticket - ..passwordSaved = - savePW || (deletePW ? false : b.passwordSaved ?? false) - ..hostname = hostname)); - id = widget.userModel!.identifier; - } - - if (id != null) { - try { - if (savePW) { - await savePassword(id, enteredPassword); - } else if (deletePW) { - await deletePassword(id); - } - } catch (e) { - if (!mounted) return; - await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Password saving error'), - scrollable: true, - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Could not save or delete password.'), - ExpansionTile( - title: const Text('Details'), - children: [Text(e.toString())], - ) - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Continue')), - ], - )); - } - } - await loginStorage.saveToDisk(); - - if (mounted) { - Navigator.of(context).pop(client); - } + await _finishLogin( + client, + realm: realm!, + username: username, + enteredPassword: enteredPassword, + savedPassword: savedPassword, + ); } on proxclient.ProxmoxApiException catch (e) { print(e); if (!mounted) return; @@ -618,6 +533,110 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> { }); } + /// Common tail of the login flow once an authenticated client exists: + /// handles a pending TFA challenge, fetches cluster status, persists the + /// login and closes the login page. Returns early (without closing the + /// page) if the user cancels a TFA challenge. + Future<void> _finishLogin( + ProxmoxApiClient client, { + required String realm, + required String username, + String enteredPassword = '', + String? savedPassword, + }) async { + if (client.credentials.tfa != null && + client.credentials.tfa!.kinds().isNotEmpty) { + if (!mounted) return; + ProxmoxApiClient? tfaclient = + await Navigator.of(context).push(MaterialPageRoute( + builder: (context) => ProxmoxTfaForm( + apiClient: client, + ), + )); + + if (tfaclient != null) { + client = tfaclient; + } else { + return; + } + } + + final status = await client.getClusterStatus(); + final hostname = + status.singleWhereOrNull((element) => element.local ?? false)?.name; + var loginStorage = await ProxmoxLoginStorage.fromLocalStorage(); + + final savePW = enteredPassword != '' && + _savePasswordCB && + enteredPassword != savedPassword; + final deletePW = enteredPassword != '' && !savePW && !_savePasswordCB; + String? id; + + final origin = normalizeUrl(_originController.text.trim()); + + if (widget.isCreate!) { + final newLogin = ProxmoxLoginModel((b) => b + ..origin = origin + ..username = username + ..realm = realm + ..productType = ProxmoxProductType.pve + ..ticket = client.credentials.ticket + ..passwordSaved = savePW + ..hostname = hostname); + + loginStorage = loginStorage!.rebuild((b) => b..logins.add(newLogin)); + id = newLogin.identifier; + } else { + loginStorage = loginStorage!.rebuild((b) => b + ..logins.rebuildWhere( + (m) => m == widget.userModel, + (b) => b + ..ticket = client.credentials.ticket + ..passwordSaved = + savePW || (deletePW ? false : b.passwordSaved ?? false) + ..hostname = hostname)); + id = widget.userModel!.identifier; + } + + if (id != null) { + try { + if (savePW) { + await savePassword(id, enteredPassword); + } else if (deletePW) { + await deletePassword(id); + } + } catch (e) { + if (!mounted) return; + await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Password saving error'), + scrollable: true, + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Could not save or delete password.'), + ExpansionTile( + title: const Text('Details'), + children: [Text(e.toString())], + ) + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Continue')), + ], + )); + } + } + await loginStorage.saveToDisk(); + + if (mounted) { + Navigator.of(context).pop(client); + } + } + Future<List<PveAccessDomainModel?>?> _loadAccessDomains(Uri uri) async { final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage(); List<PveAccessDomainModel?>? response; -- 2.50.1 (Apple Git-155) ^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH login-manager v3 3/5] refactor: ui: split password form into its own widget 2026-08-21 3:41 ` [PATCH v3 00/10] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque ` (3 preceding siblings ...) 2026-08-21 3:41 ` [PATCH login-manager v3 2/5] refactor: ui: factor out shared login tail into _finishLogin Azharul Haque @ 2026-08-21 3:41 ` Azharul Haque 2026-08-21 3:41 ` [PATCH login-manager v3 4/5] fix #4281: ui: add OpenID Connect login flow to login form Azharul Haque ` (4 subsequent siblings) 9 siblings, 0 replies; 33+ messages in thread From: Azharul Haque @ 2026-08-21 3:41 UTC (permalink / raw) To: pve-devel; +Cc: haque Group the username field together with the password field below the realm selector (it previously sat above it), and extract the credential fields -- username, password and the save-password checkbox -- into a private _ProxmoxPasswordForm widget. No functional change. This prepares for OpenID Connect realm support in the following commit: an OpenID realm has no username/password to enter in the app, so the credential fields need to be shown or replaced as one unit depending on the selected realm's type. Suggested-by: Shan Shaji <s.shaji@proxmox.com> Signed-off-by: Azharul Haque <haque@azharul.com> --- lib/proxmox_login_form.dart | 169 ++++++++++++++++++++++-------------- 1 file changed, 105 insertions(+), 64 deletions(-) diff --git a/lib/proxmox_login_form.dart b/lib/proxmox_login_form.dart index 6039407..8d8f2da 100644 --- a/lib/proxmox_login_form.dart +++ b/lib/proxmox_login_form.dart @@ -63,10 +63,6 @@ class ProxmoxLoginForm extends StatefulWidget { } class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> { - bool _obscure = true; - bool? _savePwCheckbox; - FocusNode? passwordFocusNode; - @override Widget build(BuildContext context) { if (widget.accessDomains == null) { @@ -97,20 +93,6 @@ class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> { controller: widget.originController, enabled: false, ), - TextFormField( - decoration: const InputDecoration( - icon: Icon(Icons.person), - labelText: 'Username', - ), - controller: widget.usernameController, - validator: (value) { - if (value!.isEmpty) { - return 'Please enter username'; - } - return null; - }, - autofillHints: const [AutofillHints.username], - ), DropdownButtonFormField( decoration: const InputDecoration(icon: Icon(Icons.domain)), items: widget.accessDomains! @@ -127,56 +109,115 @@ class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> { widget.accessDomains!.map((e) => Text(e!.realm)).toList(), initialValue: widget.selectedDomain, ), - Stack( - children: [ - TextFormField( - decoration: const InputDecoration( - icon: Icon(Icons.lock), - labelText: 'Password', - ), - controller: widget.passwordController, - obscureText: _obscure, - autocorrect: false, - focusNode: passwordFocusNode, - validator: (value) { - if (value!.isEmpty) { - return 'Please enter password'; - } - return null; - }, - onFieldSubmitted: (value) => widget.onPasswordSubmitted!(), - autofillHints: const [AutofillHints.password], - ), - Align( - alignment: Alignment.bottomRight, - child: IconButton( - constraints: BoxConstraints.tight(const Size(58, 58)), - iconSize: 24, - tooltip: _obscure ? "Show password" : "Hide password", - icon: - Icon(_obscure ? Icons.visibility : Icons.visibility_off), - onPressed: () => setState(() { - _obscure = !_obscure; - }), - ), - ) - ], + _ProxmoxPasswordForm( + usernameController: widget.usernameController, + passwordController: widget.passwordController, + onPasswordSubmitted: widget.onPasswordSubmitted, + onSavePasswordChanged: widget.onSavePasswordChanged, + canSavePassword: widget.canSavePassword, + passwordSaved: widget.passwordSaved, ), - if (widget.canSavePassword ?? false) - CheckboxListTile( - title: const Text('Save password in biometric storage'), - value: _savePwCheckbox ?? widget.passwordSaved ?? false, - onChanged: (value) { - if (widget.onSavePasswordChanged != null) { - widget.onSavePasswordChanged!(value!); + ], + ), + ); + } +} + +/// Username/password credential fields of the login form, including the +/// optional "save password" checkbox. +class _ProxmoxPasswordForm extends StatefulWidget { + final TextEditingController usernameController; + final TextEditingController passwordController; + final Function? onPasswordSubmitted; + final Function? onSavePasswordChanged; + final bool? canSavePassword; + final bool? passwordSaved; + + const _ProxmoxPasswordForm({ + required this.usernameController, + required this.passwordController, + this.onPasswordSubmitted, + this.onSavePasswordChanged, + this.canSavePassword, + this.passwordSaved, + }); + + @override + State<_ProxmoxPasswordForm> createState() => _ProxmoxPasswordFormState(); +} + +class _ProxmoxPasswordFormState extends State<_ProxmoxPasswordForm> { + bool _obscure = true; + bool? _savePwCheckbox; + FocusNode? passwordFocusNode; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: const InputDecoration( + icon: Icon(Icons.person), + labelText: 'Username', + ), + controller: widget.usernameController, + validator: (value) { + if (value!.isEmpty) { + return 'Please enter username'; + } + return null; + }, + autofillHints: const [AutofillHints.username], + ), + Stack( + children: [ + TextFormField( + decoration: const InputDecoration( + icon: Icon(Icons.lock), + labelText: 'Password', + ), + controller: widget.passwordController, + obscureText: _obscure, + autocorrect: false, + focusNode: passwordFocusNode, + validator: (value) { + if (value!.isEmpty) { + return 'Please enter password'; } - setState(() { - _savePwCheckbox = value!; - }); + return null; }, + onFieldSubmitted: (value) => widget.onPasswordSubmitted!(), + autofillHints: const [AutofillHints.password], + ), + Align( + alignment: Alignment.bottomRight, + child: IconButton( + constraints: BoxConstraints.tight(const Size(58, 58)), + iconSize: 24, + tooltip: _obscure ? "Show password" : "Hide password", + icon: Icon(_obscure ? Icons.visibility : Icons.visibility_off), + onPressed: () => setState(() { + _obscure = !_obscure; + }), + ), ) - ], - ), + ], + ), + if (widget.canSavePassword ?? false) + CheckboxListTile( + title: const Text('Save password in biometric storage'), + value: _savePwCheckbox ?? widget.passwordSaved ?? false, + onChanged: (value) { + if (widget.onSavePasswordChanged != null) { + widget.onSavePasswordChanged!(value!); + } + setState(() { + _savePwCheckbox = value!; + }); + }, + ) + ], ); } -- 2.50.1 (Apple Git-155) ^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH login-manager v3 4/5] fix #4281: ui: add OpenID Connect login flow to login form 2026-08-21 3:41 ` [PATCH v3 00/10] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque ` (4 preceding siblings ...) 2026-08-21 3:41 ` [PATCH login-manager v3 3/5] refactor: ui: split password form into its own widget Azharul Haque @ 2026-08-21 3:41 ` Azharul Haque 2026-08-21 3:41 ` [PATCH login-manager v3 5/5] fix #4281: ui: fix stale Continue button state on realm switch Azharul Haque ` (3 subsequent siblings) 9 siblings, 0 replies; 33+ messages in thread From: Azharul Haque @ 2026-08-21 3:41 UTC (permalink / raw) To: pve-devel; +Cc: haque The native Flutter app never implemented OpenID Connect / OAuth realm login: selecting an OAuth realm just showed username/password fields that could never work. Add an OpenID branch alongside the password form: realms of type PveAccessDomainType.openid show an explanatory message instead of credential fields, and Continue drives the OAuth flow via flutter_web_auth_2 (system browser / ASWebAuthenticationSession on iOS, Chrome Custom Tabs on Android -- deliberately not an in-app webview) instead of calling authenticate(). The callback scheme is introduced directly as com.proxmox.app here, since it is namespaced under Proxmox's own reserved package prefix from the start. _onOpenIdLoginButtonPressed shares _finishLogin with the password flow once an authenticated client exists. Signed-off-by: Azharul Haque <haque@azharul.com> --- lib/proxmox_login_form.dart | 115 +++++++++++++++++++++++++++++++++--- 1 file changed, 106 insertions(+), 9 deletions(-) diff --git a/lib/proxmox_login_form.dart b/lib/proxmox_login_form.dart index 8d8f2da..1a94445 100644 --- a/lib/proxmox_login_form.dart +++ b/lib/proxmox_login_form.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:collection/collection.dart'; import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart' as proxclient; @@ -12,6 +13,17 @@ import 'package:proxmox_login_manager/proxmox_tfa_form.dart'; import 'package:proxmox_login_manager/extension.dart'; import 'package:proxmox_login_manager/proxmox_password_store.dart'; +/// Custom URL scheme the identity provider redirects back to once an +/// OpenID Connect login completes. Must be registered as a valid redirect +/// URI with the realm's provider, as well as in the Android manifest (iOS +/// needs no static registration, ASWebAuthenticationSession handles the +/// scheme dynamically). +/// +/// Derived from the app's own package/bundle identifier (`com.proxmox.*`, +/// reserved for Proxmox on both app stores) rather than an arbitrary word, +/// so it can't collide with another app's custom URL scheme. +const String openIdCallbackScheme = 'com.proxmox.app'; + class ProxmoxProgressModel { int inProgress = 0; String message = 'Loading...'; @@ -109,14 +121,25 @@ class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> { widget.accessDomains!.map((e) => Text(e!.realm)).toList(), initialValue: widget.selectedDomain, ), - _ProxmoxPasswordForm( - usernameController: widget.usernameController, - passwordController: widget.passwordController, - onPasswordSubmitted: widget.onPasswordSubmitted, - onSavePasswordChanged: widget.onSavePasswordChanged, - canSavePassword: widget.canSavePassword, - passwordSaved: widget.passwordSaved, - ), + switch (widget.selectedDomain?.type) { + PveAccessDomainType.openid => Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Text( + "This realm signs you in through your browser. " + "Tap Continue to proceed.", + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + ), + _ => _ProxmoxPasswordForm( + usernameController: widget.usernameController, + passwordController: widget.passwordController, + onPasswordSubmitted: widget.onPasswordSubmitted, + onSavePasswordChanged: widget.onSavePasswordChanged, + canSavePassword: widget.canSavePassword, + passwordSaved: widget.passwordSaved, + ), + }, ], ), ); @@ -459,7 +482,13 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> { }); if (isValid) { if (snapshot.hasData) { - _onLoginButtonPressed(); + if (_selectedDomain + ?.isOpenIdRealm == + true) { + _onOpenIdLoginButtonPressed(); + } else { + _onLoginButtonPressed(); + } } else { setState(() { _accessDomains = @@ -574,6 +603,74 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> { }); } + Future<void> _onOpenIdLoginButtonPressed() async { + setState(() { + _progressModel + ..inProgress += 1 + ..message = 'Connecting to identity provider...'; + }); + + try { + final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage(); + final origin = normalizeUrl(_originController.text.trim()); + final realm = _selectedDomain!.realm; + final redirectUrl = + Uri(scheme: openIdCallbackScheme, host: 'openid-callback'); + + final authUrl = await proxclient.openIdAuthUrl( + realm, origin, redirectUrl, settings.sslValidation!); + + final result = await FlutterWebAuth2.authenticate( + url: authUrl, + callbackUrlScheme: openIdCallbackScheme, + ); + + final callbackUri = Uri.parse(result); + final state = callbackUri.queryParameters['state']; + final code = callbackUri.queryParameters['code']; + if (state == null || code == null) { + throw proxclient.ProxmoxApiException( + 'Identity provider did not return an authorization code', 400); + } + + final client = await proxclient.openIdLogin( + state, code, origin, redirectUrl, settings.sslValidation!); + + final fullUsername = client.credentials.username; + final username = fullUsername.contains('@') + ? fullUsername.substring(0, fullUsername.lastIndexOf('@')) + : fullUsername; + + await _finishLogin(client, realm: realm, username: username); + } on proxclient.ProxmoxApiException catch (e) { + if (mounted) { + showDialog( + context: context, + builder: (context) => ProxmoxApiErrorDialog( + exception: e, + ), + ); + } + } catch (e) { + if (mounted) { + if (e.runtimeType == HandshakeException) { + showDialog( + context: context, + builder: (context) => const ProxmoxCertificateErrorDialog(), + ); + } else { + showDialog( + context: context, + builder: (context) => ConnectionErrorDialog(exception: e), + ); + } + } + } + setState(() { + _progressModel.inProgress -= 1; + }); + } + /// Common tail of the login flow once an authenticated client exists: /// handles a pending TFA challenge, fetches cluster status, persists the /// login and closes the login page. Returns early (without closing the -- 2.50.1 (Apple Git-155) ^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH login-manager v3 5/5] fix #4281: ui: fix stale Continue button state on realm switch 2026-08-21 3:41 ` [PATCH v3 00/10] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque ` (5 preceding siblings ...) 2026-08-21 3:41 ` [PATCH login-manager v3 4/5] fix #4281: ui: add OpenID Connect login flow to login form Azharul Haque @ 2026-08-21 3:41 ` Azharul Haque 2026-08-21 3:41 ` [PATCH flutter-frontend v3 1/3] chore: regenerate plugin registrant for flutter_web_auth_2 Azharul Haque ` (2 subsequent siblings) 9 siblings, 0 replies; 33+ messages in thread From: Azharul Haque @ 2026-08-21 3:41 UTC (permalink / raw) To: pve-devel; +Cc: haque Form(onChanged: ...) revalidates against whatever fields are currently mounted at the moment a FormField's own value changes. For the realm dropdown that runs before the following rebuild adds/removes the username/password fields for the newly selected realm's type, so Continue got enabled/disabled based on the outgoing realm's field set rather than the incoming one. Most visibly, switching from an OpenID realm to a password realm left Continue enabled with both fields empty, only failing validation once actually pressed. Recompute _submitButtonEnabled explicitly in onDomainChanged instead: OpenID realms have nothing to validate, so enable it directly; other realms are revalidated in a post-frame callback once the rebuild has settled. Do the same after the initial realm auto-selection in _getAccessDomains(), in case the default realm is an OpenID one. Signed-off-by: Azharul Haque <haque@azharul.com> --- lib/proxmox_login_form.dart | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lib/proxmox_login_form.dart b/lib/proxmox_login_form.dart index 1a94445..a634908 100644 --- a/lib/proxmox_login_form.dart +++ b/lib/proxmox_login_form.dart @@ -439,6 +439,31 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> { setState(() { _selectedDomain = value; }); + // The Form's onChanged callback validates + // against whatever fields are mounted at + // the moment the dropdown's own value + // changes, which runs before this rebuild + // adds/removes the username/password + // fields for the newly selected realm. + // Recompute once that rebuild has + // happened so we validate the field set + // that's actually showing. + if (value?.isOpenIdRealm == true) { + setState(() { + _submitButtonEnabled = true; + }); + } else { + WidgetsBinding.instance + .addPostFrameCallback((_) { + if (!mounted) return; + setState(() { + _submitButtonEnabled = _formKey + .currentState + ?.validate() ?? + false; + }); + }); + } }, onOriginSubmitted: () { final isValid = @@ -868,6 +893,9 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> { setState(() { _progressModel.inProgress -= 1; _selectedDomain = selection; + // An OpenID realm has no username/password to fill in, so there's + // nothing for the form to validate before Continue is usable. + _submitButtonEnabled = selection?.isOpenIdRealm == true; }); return response; -- 2.50.1 (Apple Git-155) ^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH flutter-frontend v3 1/3] chore: regenerate plugin registrant for flutter_web_auth_2 2026-08-21 3:41 ` [PATCH v3 00/10] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque ` (6 preceding siblings ...) 2026-08-21 3:41 ` [PATCH login-manager v3 5/5] fix #4281: ui: fix stale Continue button state on realm switch Azharul Haque @ 2026-08-21 3:41 ` Azharul Haque 2026-08-21 3:41 ` [PATCH flutter-frontend v3 2/3] fix #4281: android: set taskAffinity="" on MainActivity Azharul Haque 2026-08-21 3:41 ` [PATCH flutter-frontend v3 3/3] fix #4281: android: register OpenID Connect callback activity Azharul Haque 9 siblings, 0 replies; 33+ messages in thread From: Azharul Haque @ 2026-08-21 3:41 UTC (permalink / raw) To: pve-devel; +Cc: haque proxmox_login_manager now depends on flutter_web_auth_2, used to implement the OpenID Connect login flow added there. Regenerate the Linux plugin registrant (which is checked into this repo, unlike its Android/iOS counterparts) and pubspec.lock to pick up that transitive dependency and its desktop_webview_window fallback. Generated against Flutter 3.41 to match this project's toolchain. Signed-off-by: Azharul Haque <haque@azharul.com> --- linux/flutter/generated_plugin_registrant.cc | 8 +++ linux/flutter/generated_plugins.cmake | 2 + pubspec.lock | 60 +++++++++++++++----- 3 files changed, 56 insertions(+), 14 deletions(-) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e3392af..5d804a0 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,13 +7,21 @@ #include "generated_plugin_registrant.h" #include <biometric_storage/biometric_storage_plugin.h> +#include <desktop_webview_window/desktop_webview_window_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h> +#include <window_to_front/window_to_front_plugin.h> void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) biometric_storage_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "BiometricStoragePlugin"); biometric_storage_plugin_register_with_registrar(biometric_storage_registrar); + g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin"); + desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) window_to_front_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowToFrontPlugin"); + window_to_front_plugin_register_with_registrar(window_to_front_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 5a42b12..cc58f44 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,7 +4,9 @@ list(APPEND FLUTTER_PLUGIN_LIST biometric_storage + desktop_webview_window url_launcher_linux + window_to_front ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/pubspec.lock b/pubspec.lock index def499d..3016523 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -133,10 +133,10 @@ packages: dependency: transitive description: name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.2.1" code_builder: dependency: transitive description: @@ -201,6 +201,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.7" + desktop_webview_window: + dependency: transitive + description: + name: desktop_webview_window + sha256: b6fdae2cbf9571879b1761c12f27facaf82e22d0bdc74d049907c2a09a432957 + url: "https://pub.dev" + source: hosted + version: "0.3.0" fake_async: dependency: transitive description: @@ -315,6 +323,22 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_auth_2: + dependency: transitive + description: + name: flutter_web_auth_2 + sha256: a7655829251ee63aae64a748f8512f36670d8eba4657b0055cdf5ef304a9d164 + url: "https://pub.dev" + source: hosted + version: "5.1.0" + flutter_web_auth_2_platform_interface: + dependency: transitive + description: + name: flutter_web_auth_2_platform_interface + sha256: ba0fbba55bffb47242025f96852ad1ffba34bc451568f56ef36e613612baffab + url: "https://pub.dev" + source: hosted + version: "5.0.0" flutter_web_plugins: dependency: transitive description: flutter @@ -348,10 +372,10 @@ packages: dependency: transitive description: name: hooks - sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "2.0.2" http: dependency: transitive description: @@ -488,14 +512,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" - url: "https://pub.dev" - source: hosted - version: "0.17.6" nested: dependency: transitive description: @@ -508,10 +524,10 @@ packages: dependency: transitive description: name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e url: "https://pub.dev" source: hosted - version: "9.3.0" + version: "9.5.0" package_config: dependency: transitive description: @@ -638,6 +654,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" retry: dependency: transitive description: @@ -923,6 +947,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.15.0" + window_to_front: + dependency: transitive + description: + name: window_to_front + sha256: "14fad8984db4415e2eeb30b04bb77140b180e260d6cb66b26de126a8657a9241" + url: "https://pub.dev" + source: hosted + version: "0.0.4" xdg_directories: dependency: transitive description: -- 2.50.1 (Apple Git-155) ^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH flutter-frontend v3 2/3] fix #4281: android: set taskAffinity="" on MainActivity 2026-08-21 3:41 ` [PATCH v3 00/10] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque ` (7 preceding siblings ...) 2026-08-21 3:41 ` [PATCH flutter-frontend v3 1/3] chore: regenerate plugin registrant for flutter_web_auth_2 Azharul Haque @ 2026-08-21 3:41 ` Azharul Haque 2026-08-21 3:41 ` [PATCH flutter-frontend v3 3/3] fix #4281: android: register OpenID Connect callback activity Azharul Haque 9 siblings, 0 replies; 33+ messages in thread From: Azharul Haque @ 2026-08-21 3:41 UTC (permalink / raw) To: pve-devel; +Cc: haque Recommended by flutter_web_auth_2 for the version this project uses, to avoid a stray task appearing after the OpenID callback returns control to MainActivity once the CallbackActivity added in the following commit hands off. Harmless on its own ahead of that, since there is currently only ever one activity in this app's task. Signed-off-by: Azharul Haque <haque@azharul.com> --- android/app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 66135eb..654f13d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ android:name="com.proxmox.app.pve_flutter_frontend.MainActivity" android:exported="true" android:launchMode="singleTop" + android:taskAffinity="" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" -- 2.50.1 (Apple Git-155) ^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH flutter-frontend v3 3/3] fix #4281: android: register OpenID Connect callback activity 2026-08-21 3:41 ` [PATCH v3 00/10] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque ` (8 preceding siblings ...) 2026-08-21 3:41 ` [PATCH flutter-frontend v3 2/3] fix #4281: android: set taskAffinity="" on MainActivity Azharul Haque @ 2026-08-21 3:41 ` Azharul Haque 9 siblings, 0 replies; 33+ messages in thread From: Azharul Haque @ 2026-08-21 3:41 UTC (permalink / raw) To: pve-devel; +Cc: haque proxmox_login_manager's OpenID login flow (see the corresponding patch there) opens the provider's authorization URL via flutter_web_auth_2 and expects the provider's redirect back to com.proxmox.app://openid-callback to be captured by the app. On Android this requires explicitly registering flutter_web_auth_2's CallbackActivity for that scheme; add it, following the plugin's setup instructions. No changes needed on iOS: ASWebAuthenticationSession handles the custom-scheme redirect without any Info.plist registration. Signed-off-by: Azharul Haque <haque@azharul.com> --- android/app/src/main/AndroidManifest.xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 654f13d..658f916 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -24,6 +24,21 @@ </intent-filter> </activity> + <!-- Captures the OpenID Connect provider's redirect back into the app + for realms configured with an OAuth/OIDC login, see + flutter_web_auth_2 and ProxmoxLoginForm's OpenID login flow. --> + <activity + android:name="com.linusu.flutter_web_auth_2.CallbackActivity" + android:exported="true" + android:taskAffinity=""> + <intent-filter android:label="flutter_web_auth_2"> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="com.proxmox.app" /> + </intent-filter> + </activity> + <!-- This is used by Flutter to generate GeneratedPluginRegistrant.java --> <meta-data android:name="flutterEmbedding" -- 2.50.1 (Apple Git-155) ^ permalink raw reply related [flat|nested] 33+ messages in thread
end of thread, other threads:[~2026-08-25 8:11 UTC | newest] Thread overview: 33+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-08-10 5:40 [PATCH 0/2] android: register OpenID Connect callback activity for #4281 Azharul Haque 2026-08-10 5:40 ` [PATCH 1/2] fix #4281: android: register OpenID Connect callback activity Azharul Haque 2026-08-10 5:40 ` [PATCH 2/2] fix #4281: android: match renamed OpenID callback scheme Azharul Haque 2026-08-10 13:59 ` [PATCH 0/2] android: register OpenID Connect callback activity for #4281 Shan Shaji 2026-08-10 14:47 ` [PATCH v2 0/7] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque 2026-08-10 14:47 ` [PATCH dart-api-client v2 1/2] fix #4281: access: add `type` property to `PveAccessDomainModel` Azharul Haque 2026-08-20 9:17 ` Shan Shaji 2026-08-20 13:37 ` Azharul Haque 2026-08-20 14:39 ` Shan Shaji 2026-08-21 2:56 ` Azharul Haque 2026-08-21 8:00 ` Shan Shaji 2026-08-10 14:47 ` [PATCH dart-api-client v2 2/2] fix #4281: access: add OpenID Connect auth-url/login helpers Azharul Haque 2026-08-20 9:48 ` Shan Shaji 2026-08-20 13:41 ` Azharul Haque 2026-08-10 14:47 ` [PATCH login-manager v2 1/3] fix #4281: ui: add OpenID Connect login flow to login form Azharul Haque 2026-08-20 12:31 ` Shan Shaji 2026-08-10 14:47 ` [PATCH login-manager v2 2/3] fix #4281: ui: fix stale Continue button state on realm switch Azharul Haque 2026-08-10 14:47 ` [PATCH login-manager v2 3/3] fix #4281: ui: use a namespaced OpenID callback scheme Azharul Haque 2026-08-20 14:28 ` Shan Shaji 2026-08-10 14:47 ` [PATCH flutter-frontend v2 1/2] fix #4281: android: register OpenID Connect callback activity Azharul Haque 2026-08-20 14:01 ` Shan Shaji 2026-08-10 14:47 ` [PATCH flutter-frontend v2 2/2] fix #4281: android: match renamed OpenID callback scheme Azharul Haque 2026-08-21 3:41 ` [PATCH v3 00/10] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque 2026-08-21 3:41 ` [PATCH dart-api-client v3 1/2] fix #4281: access: add `type` property to `PveAccessDomainModel` Azharul Haque 2026-08-21 3:41 ` [PATCH dart-api-client v3 2/2] fix #4281: access: add OpenID Connect auth-url/login helpers Azharul Haque 2026-08-21 3:41 ` [PATCH login-manager v3 1/5] fix #4281: deps: add flutter_web_auth_2 dependency Azharul Haque 2026-08-21 3:41 ` [PATCH login-manager v3 2/5] refactor: ui: factor out shared login tail into _finishLogin Azharul Haque 2026-08-21 3:41 ` [PATCH login-manager v3 3/5] refactor: ui: split password form into its own widget Azharul Haque 2026-08-21 3:41 ` [PATCH login-manager v3 4/5] fix #4281: ui: add OpenID Connect login flow to login form Azharul Haque 2026-08-21 3:41 ` [PATCH login-manager v3 5/5] fix #4281: ui: fix stale Continue button state on realm switch Azharul Haque 2026-08-21 3:41 ` [PATCH flutter-frontend v3 1/3] chore: regenerate plugin registrant for flutter_web_auth_2 Azharul Haque 2026-08-21 3:41 ` [PATCH flutter-frontend v3 2/3] fix #4281: android: set taskAffinity="" on MainActivity Azharul Haque 2026-08-21 3:41 ` [PATCH flutter-frontend v3 3/3] fix #4281: android: register OpenID Connect callback activity Azharul Haque
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox