all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH 0/3] ui: implement OpenID Connect login flow for #4281
@ 2026-08-10  5:39 Azharul Haque
  2026-08-10  5:39 ` [PATCH 1/3] fix #4281: ui: add OpenID Connect login flow to login form Azharul Haque
                   ` (2 more replies)
  0 siblings, 3 replies; 4+ messages in thread
From: Azharul Haque @ 2026-08-10  5:39 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.

This series adds the login-form side of OIDC support:

  - 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)
  - fix a stale Continue button enabled/disabled state when switching
    realms mid-flow
  - namespace the OpenID redirect scheme under Proxmox's own reserved
    com.proxmox.* package prefix, to avoid custom-URL-scheme
    collisions with other apps on Android

Depends on the `type` property added to PveAccessDomainModel in the
companion proxmox_dart_api_client series. A further companion series
to pve_flutter_frontend registers the Android-side callback activity
for the scheme used here; no iOS-side wiring is required, since
ASWebAuthenticationSession resolves the custom-scheme redirect at
runtime without a static declaration.

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 (3):
  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

 lib/proxmox_login_form.dart | 451 ++++++++++++++++++++++++------------
 pubspec.lock                | 160 +++++++++++--
 pubspec.yaml                |   1 +
 3 files changed, 441 insertions(+), 171 deletions(-)

-- 
2.50.1 (Apple Git-155)




^ permalink raw reply	[flat|nested] 4+ messages in thread

* [PATCH 1/3] fix #4281: ui: add OpenID Connect login flow to login form
  2026-08-10  5:39 [PATCH 0/3] ui: implement OpenID Connect login flow for #4281 Azharul Haque
@ 2026-08-10  5:39 ` Azharul Haque
  2026-08-10  5:39 ` [PATCH 2/3] fix #4281: ui: fix stale Continue button state on realm switch Azharul Haque
  2026-08-10  5:39 ` [PATCH 3/3] fix #4281: ui: use a namespaced OpenID callback scheme Azharul Haque
  2 siblings, 0 replies; 4+ messages in thread
From: Azharul Haque @ 2026-08-10  5:39 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] 4+ messages in thread

* [PATCH 2/3] fix #4281: ui: fix stale Continue button state on realm switch
  2026-08-10  5:39 [PATCH 0/3] ui: implement OpenID Connect login flow for #4281 Azharul Haque
  2026-08-10  5:39 ` [PATCH 1/3] fix #4281: ui: add OpenID Connect login flow to login form Azharul Haque
@ 2026-08-10  5:39 ` Azharul Haque
  2026-08-10  5:39 ` [PATCH 3/3] fix #4281: ui: use a namespaced OpenID callback scheme Azharul Haque
  2 siblings, 0 replies; 4+ messages in thread
From: Azharul Haque @ 2026-08-10  5:39 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] 4+ messages in thread

* [PATCH 3/3] fix #4281: ui: use a namespaced OpenID callback scheme
  2026-08-10  5:39 [PATCH 0/3] ui: implement OpenID Connect login flow for #4281 Azharul Haque
  2026-08-10  5:39 ` [PATCH 1/3] fix #4281: ui: add OpenID Connect login flow to login form Azharul Haque
  2026-08-10  5:39 ` [PATCH 2/3] fix #4281: ui: fix stale Continue button state on realm switch Azharul Haque
@ 2026-08-10  5:39 ` Azharul Haque
  2 siblings, 0 replies; 4+ messages in thread
From: Azharul Haque @ 2026-08-10  5:39 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] 4+ messages in thread

end of thread, other threads:[~2026-08-10 12:37 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-10  5:39 [PATCH 0/3] ui: implement OpenID Connect login flow for #4281 Azharul Haque
2026-08-10  5:39 ` [PATCH 1/3] fix #4281: ui: add OpenID Connect login flow to login form Azharul Haque
2026-08-10  5:39 ` [PATCH 2/3] fix #4281: ui: fix stale Continue button state on realm switch Azharul Haque
2026-08-10  5:39 ` [PATCH 3/3] fix #4281: ui: use a namespaced OpenID callback scheme Azharul Haque

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal