public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Azharul Haque <haque@azharul.com>
To: pve-devel@lists.proxmox.com
Cc: haque@azharul.com
Subject: [PATCH 1/3] fix #4281: ui: add OpenID Connect login flow to login form
Date: Mon, 10 Aug 2026 01:39:39 -0400	[thread overview]
Message-ID: <20260810053941.17000-2-haque@azharul.com> (raw)
In-Reply-To: <20260810053941.17000-1-haque@azharul.com>

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)




  reply	other threads:[~2026-08-10 12:36 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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 [this message]
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

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260810053941.17000-2-haque@azharul.com \
    --to=haque@azharul.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal