From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id 0C1141FF0C1 for ; Tue, 25 Aug 2026 10:10:07 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id E6D812173E; Tue, 25 Aug 2026 10:09:12 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=azharul.com; s=t28hkp5; t=1787283742; x=1787888542; darn=lists.proxmox.com; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=pYtkxG32JOKiZliyiQiswzJdFJy8Pm0p7gz9RWavIgY=; b=myCLJrPY9EJc40gOel6w+Z+dwpVfhl1mrCZuhhGov/EBbFDnRifXDm4ks8unCwrr7t GGIQ3JtMiELkKYBn514vlVSgfqc7Y3PkSpY5U8UrP6hwV6qjsdtYnEMEkUUmyFRwQ/zj y2JfeZj8wLDsQ0qNYl3NxkfNdYH0d9iHmuxIiRPDczDZhiQQuG1S1/eoRn+NcCoU1aFf uuKH1T8YLojAdBnWlP/B6murRFCAPsp5XZMmz6QZjzFdlaRXP/Gn/wOlmviSe+aGzeg1 4twKa7gOZqiU1hX03/jMi5yjSvZ2lrzPOr3Lg3NaWnMIh/vFkGgaEGzgfcGuSeJtMCcJ 2gwQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1787283742; x=1787888542; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=pYtkxG32JOKiZliyiQiswzJdFJy8Pm0p7gz9RWavIgY=; b=E+pT81mtt2TIv2d9Y1/wv3P3lfAt0ojSUruaiQaxanG08TTffmoqt1AYdwDPvKWkPk DcfeDZ6lFkOCjVZd32qWVcFk0PcDZBR6ge9bO0QIbGoJFMEmjrEpD0lqwTiaDwSZ/QLk mryLl0JQvWtRhliQARLrwjLyRKgli5RPDtkW4j8sZ+uie8wOAQT2xQByc3Z67pqlYO88 48Ev6VOoWRgsD6vOlQmNl00J2zj3gDTC2LiJoSS5J1UM+zng4SzwSG9JRpovIww6HFH/ GDvkF08WJF0/ZcpZ7j6Ky286CgvxoMO13YUusjWWcsne17IMDXnzwu9T5kuqxxgqphC7 XVNQ== X-Gm-Message-State: AOJu0YwBFFOtpxkw1hsoHyzIiUwTdOdKGFjoVtXdnXTcVJBLqNV2qsIk VCi2L/tx6gcehDGL+gC2JioWau/n7O+1/tE8iB3Xv1yRYum50jRx2EZjkoTBDPjn2NbwMLaJ2Sb jdsA4uw== X-Gm-Gg: AR+sD13Z4ZIraNex6LM5cwkmBYGRIWK7Z5N31sWyFeU9Em1MU4oi9etXUGJtSE9o9Py bGsTfz7lschhhnU6LBuF4nOapiweDAVs9e+lp/KhCFHD2tOHk9oKusEyk3x9aCCFPQO2P7raU06 FiQcy9khSGsrcDu3/gYEp6JddcEwR6GwdhhPf2PkowAq2cgBRynnjHw1bYXeuuSxKeKgz2W0ILm FXmD2fPSMnuEmbQL5kA3JKGh/IhuzHU2LJT0XhE+pu/jqvR32IYm32BBZOOlnlfTOVt2ha8MRI8 od2wIvy2cxrlRh4k+BwS2jNFN0w87WNC/1bWU2eXsiNEwlg67JCTvl4B45Sj4snJG3PSU58T+es 8xgsC9kzivPdLBnXu8qrzd/wnCCj8GAxGV0AamQ0AtUr2O0OGVV+ormUfKfVCD3rR6JW2uSSq5Q Ts5jcTOjpxFFGf+5uXW9upOrlpgr0k/CzkNioyJq+vVZ/jecBTeex4oC9ZaLwn+eTH3jGl0ZGSl dxAVjnvK9PKn8lB/ugw4XFUa0zATEUYCLt/tYbPX8OZ4tMI56iV+x2TwjFD5dn8FDBkiTKTVgIs mYVAz9ejPj/QqSIlmI8pHVxpDiomXiQD9cv1ICOQEpItGzmfNt4elTogBfoeJ74= X-Received: by 2002:a05:620a:29cf:b0:936:4c33:67f with SMTP id af79cd13be357-93739540c2dmr265827485a.19.1787283741548; Thu, 20 Aug 2026 20:42:21 -0700 (PDT) From: Azharul Haque To: pve-devel@lists.proxmox.com Subject: [PATCH login-manager v3 4/5] fix #4281: ui: add OpenID Connect login flow to login form Date: Thu, 20 Aug 2026 23:41:43 -0400 Message-ID: <20260821034147.30194-7-haque@azharul.com> X-Mailer: git-send-email 2.50.1 In-Reply-To: <20260821034147.30194-1-haque@azharul.com> References: <20260810144713.75806-1-haque@azharul.com> <20260821034147.30194-1-haque@azharul.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.205 Adjusted score from AWL reputation of From: address DKIM_SIGNED 0.1 Message has a DKIM or DK signature, not necessarily valid DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's domain DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from domain DMARC_PASS -0.1 DMARC pass policy RCVD_IN_DNSWL_NONE -0.0001 Sender listed at https://www.dnswl.org/, no trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record X-MailFrom: haque@azharul.com X-Mailman-Rule-Hits: nonmember-moderation X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation Message-ID-Hash: 46NAMU3W6IJWICKLY6V44QTPZBABY233 X-Message-ID-Hash: 46NAMU3W6IJWICKLY6V44QTPZBABY233 X-Mailman-Approved-At: Tue, 25 Aug 2026 10:08:31 +0200 CC: haque@azharul.com X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: The native Flutter app never implemented OpenID Connect / OAuth realm login: selecting an OAuth realm just showed username/password fields that could never work. Add an OpenID branch alongside the password form: realms of type PveAccessDomainType.openid show an explanatory message instead of credential fields, and Continue drives the OAuth flow via flutter_web_auth_2 (system browser / ASWebAuthenticationSession on iOS, Chrome Custom Tabs on Android -- deliberately not an in-app webview) instead of calling authenticate(). The callback scheme is introduced directly as com.proxmox.app here, since it is namespaced under Proxmox's own reserved package prefix from the start. _onOpenIdLoginButtonPressed shares _finishLogin with the password flow once an authenticated client exists. Signed-off-by: Azharul Haque --- lib/proxmox_login_form.dart | 115 +++++++++++++++++++++++++++++++++--- 1 file changed, 106 insertions(+), 9 deletions(-) diff --git a/lib/proxmox_login_form.dart b/lib/proxmox_login_form.dart index 8d8f2da..1a94445 100644 --- a/lib/proxmox_login_form.dart +++ b/lib/proxmox_login_form.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:collection/collection.dart'; import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart' as proxclient; @@ -12,6 +13,17 @@ import 'package:proxmox_login_manager/proxmox_tfa_form.dart'; import 'package:proxmox_login_manager/extension.dart'; import 'package:proxmox_login_manager/proxmox_password_store.dart'; +/// Custom URL scheme the identity provider redirects back to once an +/// OpenID Connect login completes. Must be registered as a valid redirect +/// URI with the realm's provider, as well as in the Android manifest (iOS +/// needs no static registration, ASWebAuthenticationSession handles the +/// scheme dynamically). +/// +/// Derived from the app's own package/bundle identifier (`com.proxmox.*`, +/// reserved for Proxmox on both app stores) rather than an arbitrary word, +/// so it can't collide with another app's custom URL scheme. +const String openIdCallbackScheme = 'com.proxmox.app'; + class ProxmoxProgressModel { int inProgress = 0; String message = 'Loading...'; @@ -109,14 +121,25 @@ class _ProxmoxLoginFormState extends State { widget.accessDomains!.map((e) => Text(e!.realm)).toList(), initialValue: widget.selectedDomain, ), - _ProxmoxPasswordForm( - usernameController: widget.usernameController, - passwordController: widget.passwordController, - onPasswordSubmitted: widget.onPasswordSubmitted, - onSavePasswordChanged: widget.onSavePasswordChanged, - canSavePassword: widget.canSavePassword, - passwordSaved: widget.passwordSaved, - ), + switch (widget.selectedDomain?.type) { + PveAccessDomainType.openid => Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Text( + "This realm signs you in through your browser. " + "Tap Continue to proceed.", + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + ), + _ => _ProxmoxPasswordForm( + usernameController: widget.usernameController, + passwordController: widget.passwordController, + onPasswordSubmitted: widget.onPasswordSubmitted, + onSavePasswordChanged: widget.onSavePasswordChanged, + canSavePassword: widget.canSavePassword, + passwordSaved: widget.passwordSaved, + ), + }, ], ), ); @@ -459,7 +482,13 @@ class _ProxmoxLoginPageState extends State { }); if (isValid) { if (snapshot.hasData) { - _onLoginButtonPressed(); + if (_selectedDomain + ?.isOpenIdRealm == + true) { + _onOpenIdLoginButtonPressed(); + } else { + _onLoginButtonPressed(); + } } else { setState(() { _accessDomains = @@ -574,6 +603,74 @@ class _ProxmoxLoginPageState extends State { }); } + Future _onOpenIdLoginButtonPressed() async { + setState(() { + _progressModel + ..inProgress += 1 + ..message = 'Connecting to identity provider...'; + }); + + try { + final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage(); + final origin = normalizeUrl(_originController.text.trim()); + final realm = _selectedDomain!.realm; + final redirectUrl = + Uri(scheme: openIdCallbackScheme, host: 'openid-callback'); + + final authUrl = await proxclient.openIdAuthUrl( + realm, origin, redirectUrl, settings.sslValidation!); + + final result = await FlutterWebAuth2.authenticate( + url: authUrl, + callbackUrlScheme: openIdCallbackScheme, + ); + + final callbackUri = Uri.parse(result); + final state = callbackUri.queryParameters['state']; + final code = callbackUri.queryParameters['code']; + if (state == null || code == null) { + throw proxclient.ProxmoxApiException( + 'Identity provider did not return an authorization code', 400); + } + + final client = await proxclient.openIdLogin( + state, code, origin, redirectUrl, settings.sslValidation!); + + final fullUsername = client.credentials.username; + final username = fullUsername.contains('@') + ? fullUsername.substring(0, fullUsername.lastIndexOf('@')) + : fullUsername; + + await _finishLogin(client, realm: realm, username: username); + } on proxclient.ProxmoxApiException catch (e) { + if (mounted) { + showDialog( + context: context, + builder: (context) => ProxmoxApiErrorDialog( + exception: e, + ), + ); + } + } catch (e) { + if (mounted) { + if (e.runtimeType == HandshakeException) { + showDialog( + context: context, + builder: (context) => const ProxmoxCertificateErrorDialog(), + ); + } else { + showDialog( + context: context, + builder: (context) => ConnectionErrorDialog(exception: e), + ); + } + } + } + setState(() { + _progressModel.inProgress -= 1; + }); + } + /// Common tail of the login flow once an authenticated client exists: /// handles a pending TFA challenge, fetches cluster status, persists the /// login and closes the login page. Returns early (without closing the -- 2.50.1 (Apple Git-155)