From: Azharul Haque <haque@azharul.com>
To: pve-devel@lists.proxmox.com
Cc: haque@azharul.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 [thread overview]
Message-ID: <20260821034147.30194-7-haque@azharul.com> (raw)
In-Reply-To: <20260821034147.30194-1-haque@azharul.com>
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 <haque@azharul.com>
---
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<ProxmoxLoginForm> {
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<ProxmoxLoginPage> {
});
if (isValid) {
if (snapshot.hasData) {
- _onLoginButtonPressed();
+ if (_selectedDomain
+ ?.isOpenIdRealm ==
+ true) {
+ _onOpenIdLoginButtonPressed();
+ } else {
+ _onLoginButtonPressed();
+ }
} else {
setState(() {
_accessDomains =
@@ -574,6 +603,74 @@ 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) {
+ 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)
next prev parent reply other threads:[~2026-08-25 8:10 UTC|newest]
Thread overview: 33+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-10 5:40 [PATCH 0/2] android: register OpenID Connect callback activity for #4281 Azharul Haque
2026-08-10 5:40 ` [PATCH 1/2] fix #4281: android: register OpenID Connect callback activity Azharul Haque
2026-08-10 5:40 ` [PATCH 2/2] fix #4281: android: match renamed OpenID callback scheme Azharul Haque
2026-08-10 13:59 ` [PATCH 0/2] android: register OpenID Connect callback activity for #4281 Shan Shaji
2026-08-10 14:47 ` [PATCH v2 0/7] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque
2026-08-10 14:47 ` [PATCH dart-api-client v2 1/2] fix #4281: access: add `type` property to `PveAccessDomainModel` Azharul Haque
2026-08-20 9:17 ` Shan Shaji
2026-08-20 13:37 ` Azharul Haque
2026-08-20 14:39 ` Shan Shaji
2026-08-21 2:56 ` Azharul Haque
2026-08-21 8:00 ` Shan Shaji
2026-08-10 14:47 ` [PATCH dart-api-client v2 2/2] fix #4281: access: add OpenID Connect auth-url/login helpers Azharul Haque
2026-08-20 9:48 ` Shan Shaji
2026-08-20 13:41 ` Azharul Haque
2026-08-10 14:47 ` [PATCH login-manager v2 1/3] fix #4281: ui: add OpenID Connect login flow to login form Azharul Haque
2026-08-20 12:31 ` Shan Shaji
2026-08-10 14:47 ` [PATCH login-manager v2 2/3] fix #4281: ui: fix stale Continue button state on realm switch Azharul Haque
2026-08-10 14:47 ` [PATCH login-manager v2 3/3] fix #4281: ui: use a namespaced OpenID callback scheme Azharul Haque
2026-08-20 14:28 ` Shan Shaji
2026-08-10 14:47 ` [PATCH flutter-frontend v2 1/2] fix #4281: android: register OpenID Connect callback activity Azharul Haque
2026-08-20 14:01 ` Shan Shaji
2026-08-10 14:47 ` [PATCH flutter-frontend v2 2/2] fix #4281: android: match renamed OpenID callback scheme Azharul Haque
2026-08-21 3:41 ` [PATCH v3 00/10] app: implement OpenID Connect (OAuth) realm login (#4281) Azharul Haque
2026-08-21 3:41 ` [PATCH dart-api-client v3 1/2] fix #4281: access: add `type` property to `PveAccessDomainModel` Azharul Haque
2026-08-21 3:41 ` [PATCH dart-api-client v3 2/2] fix #4281: access: add OpenID Connect auth-url/login helpers Azharul Haque
2026-08-21 3:41 ` [PATCH login-manager v3 1/5] fix #4281: deps: add flutter_web_auth_2 dependency Azharul Haque
2026-08-21 3:41 ` [PATCH login-manager v3 2/5] refactor: ui: factor out shared login tail into _finishLogin Azharul Haque
2026-08-21 3:41 ` [PATCH login-manager v3 3/5] refactor: ui: split password form into its own widget Azharul Haque
2026-08-21 3:41 ` Azharul Haque [this message]
2026-08-21 3:41 ` [PATCH login-manager v3 5/5] fix #4281: ui: fix stale Continue button state on realm switch Azharul Haque
2026-08-21 3:41 ` [PATCH flutter-frontend v3 1/3] chore: regenerate plugin registrant for flutter_web_auth_2 Azharul Haque
2026-08-21 3:41 ` [PATCH flutter-frontend v3 2/3] fix #4281: android: set taskAffinity="" on MainActivity Azharul Haque
2026-08-21 3:41 ` [PATCH flutter-frontend v3 3/3] fix #4281: android: register OpenID Connect callback activity 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=20260821034147.30194-7-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