public inbox for pve-devel@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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal