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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox