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)
next prev parent 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 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.