all lists on 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 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)




  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 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.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal