all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Alexander Abraham <a.abraham@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH proxmox_login_manager 1/1] fix #4281: login_manager: UI changes for enabling login with Open ID
Date: Tue, 29 Apr 2025 17:07:56 +0200	[thread overview]
Message-ID: <20250429150757.130996-3-a.abraham@proxmox.com> (raw)
In-Reply-To: <20250429150757.130996-1-a.abraham@proxmox.com>

This commit adds an authorization flow for logging a user in
with Open ID in the Flutter App's UI. An authorization URL
is obtained and opened in a webview. From there, the user
types in their credentials and the login is processed and
the user is logged in to the PVE app.

Signed-off-by: Alexander Abraham <a.abraham@proxmox.com>
---
 lib/proxmox_login_form.dart | 222 +++++++++++++++++++++++++++++++-----
 1 file changed, 192 insertions(+), 30 deletions(-)

diff --git a/lib/proxmox_login_form.dart b/lib/proxmox_login_form.dart
index 735bd42..7dfba9f 100644
--- a/lib/proxmox_login_form.dart
+++ b/lib/proxmox_login_form.dart
@@ -1,6 +1,7 @@
 import 'dart:io';
 import 'dart:async';
-
+import 'dart:convert';
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:collection/collection.dart';
 import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart'
@@ -12,6 +13,12 @@ import 'package:proxmox_login_manager/proxmox_login_model.dart';
 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';
+import 'package:flutter_inappwebview/flutter_inappwebview.dart';
+
+typedef AuthCallBack = Future<void> Function(
+  InAppWebViewController controller, 
+  NavigationAction navAction
+);
 
 class ProxmoxProgressModel {
   int inProgress = 0;
@@ -42,9 +49,12 @@ class ProxmoxLoginForm extends StatefulWidget {
   final Function? onSavePasswordChanged;
   final bool? canSavePassword;
   final bool? passwordSaved;
+  final bool isOIDC;
+  final bool showOIDCAuth;
 
   const ProxmoxLoginForm({
     super.key,
+    required this.isOIDC,
     required this.originController,
     required this.usernameController,
     required this.passwordController,
@@ -57,6 +67,7 @@ class ProxmoxLoginForm extends StatefulWidget {
     this.onSavePasswordChanged,
     this.canSavePassword,
     this.passwordSaved,
+    required this.showOIDCAuth
   });
 
   @override
@@ -97,7 +108,8 @@ class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> {
             controller: widget.originController,
             enabled: false,
           ),
-          TextFormField(
+          
+          if (widget.isOIDC == false) TextFormField(
             decoration: const InputDecoration(
               icon: Icon(Icons.person),
               labelText: 'Username',
@@ -123,11 +135,11 @@ class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> {
                     ))
                 .toList(),
             onChanged: widget.onDomainChanged,
-            selectedItemBuilder: (context) =>
-                widget.accessDomains!.map((e) => Text(e!.realm)).toList(),
+            selectedItemBuilder: (context) => widget.accessDomains!.map((e) =>
+                Text(e!.realm)).toList(),
             value: widget.selectedDomain,
           ),
-          Stack(
+          if (widget.isOIDC == false) Stack(
             children: [
               TextFormField(
                 decoration: const InputDecoration(
@@ -150,14 +162,18 @@ class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> {
               Align(
                 alignment: Alignment.bottomRight,
                 child: IconButton(
-                  constraints: BoxConstraints.tight(const Size(58, 58)),
+                  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;
-                  }),
+                    Icon(_obscure ? Icons.visibility : Icons.visibility_off),
+                    onPressed: () => setState(
+                      () {
+                       
+                        _obscure = !_obscure;
+                      }
+                    ),
                 ),
               )
             ],
@@ -169,12 +185,12 @@ class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> {
               onChanged: (value) {
                 if (widget.onSavePasswordChanged != null) {
                   widget.onSavePasswordChanged!(value!);
-                }
-                setState(() {
-                  _savePwCheckbox = value!;
-                });
-              },
-            )
+              }
+              setState(() {
+                _savePwCheckbox = value!;
+              });
+            },
+          )
         ],
       ),
     );
@@ -215,6 +231,11 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
   bool _submittButtonEnabled = true;
   bool _canSavePassword = false;
   bool _savePasswordCB = false;
+  bool isOIDC = false;
+  bool showOIDCAuth = false;
+  late String oidcUserName;
+  late String oidcTicket;
+  late String oidcCRSF;
 
   @override
   void initState() {
@@ -327,6 +348,7 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
                     child: FutureBuilder<List<PveAccessDomainModel?>?>(
                         future: _accessDomains,
                         builder: (context, snapshot) {
+                          
                           return Form(
                             key: _formKey,
                             onChanged: () {
@@ -338,7 +360,7 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
                             child: Column(
                               mainAxisAlignment: MainAxisAlignment.center,
                               children: [
-                                Expanded(
+                                  Expanded(
                                   child: Column(
                                     mainAxisAlignment: MainAxisAlignment.center,
                                     children: [
@@ -350,6 +372,7 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
                                   ),
                                 ),
                                 ProxmoxLoginForm(
+                                  showOIDCAuth: showOIDCAuth,
                                   originController: _originController,
                                   originValidator: (value) {
                                     if (value == null || value.isEmpty) {
@@ -364,6 +387,7 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
                                       return 'Invalid URI: $e';
                                     }
                                   },
+                                  isOIDC: isOIDC,
                                   usernameController: _usernameController,
                                   passwordController: _passwordController,
                                   accessDomains: snapshot.data,
@@ -376,6 +400,16 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
                                   onDomainChanged: (value) {
                                     setState(() {
                                       _selectedDomain = value;
+                                      if (_selectedDomain!.comment.toString() == "null"){
+                                        setState((){
+                                          isOIDC = true;
+                                          _submittButtonEnabled = true;
+                                          _canSavePassword = false;
+                                        });
+                                      }
+                                      else {
+                                        setState(() => isOIDC = false);
+                                       }
                                     });
                                   },
                                   onOriginSubmitted: () {
@@ -392,6 +426,7 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
                                   },
                                   onPasswordSubmitted: _submittButtonEnabled
                                       ? () {
+                                          
                                           final isValid =
                                               _formKey.currentState!.validate();
                                           setState(() {
@@ -411,6 +446,46 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
                                       child: TextButton(
                                         onPressed: _submittButtonEnabled
                                             ? () {
+                                                if (isOIDC) {
+                                                  Navigator.push(
+                                                   context,
+                                                    MaterialPageRoute(
+                                                      builder: (context) => 
+                                                        OIDCAuthWidget(
+                                                          realm:_selectedDomain!.realm,
+                                                          redirectUrl: _originController.text, 
+                                                          host:_originController.text,
+                                                          authHandler: (controller,navAction) async {
+                                                            Map<String,
+                                                            String> creds = parseUrl(
+                                                              navAction
+                                                                .request
+                                                                .url
+                                                                .toString()
+                                                            );
+                                                            
+                                                            String pveAuth = await fetchOIDCCredentials(
+                                                              creds["code"]!, 
+                                                              creds["state"]!, 
+                                                              creds["host"]!
+                                                            );
+                                                            Map<String,dynamic> serverCreds = jsonDecode(
+                                                              pveAuth
+                                                            )["data"]!;
+                                                            String username = serverCreds["username"]!
+                                                              .split("@")[0];
+                                                            String ticket = serverCreds["ticket"]!;
+                                                            setState((){
+                                                              _usernameController.text = username;
+                                                              _passwordController.text = ticket;
+                                                            });
+                                                            _onLoginButtonPressed();
+                                                          }
+                                                        )
+                                                      )
+                                                  );
+                                                }
+                                                else {
                                                 final isValid = _formKey
                                                     .currentState!
                                                     .validate();
@@ -428,17 +503,19 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
                                                     });
                                                   }
                                                 }
-                                              }
+                                              }}
                                             : null,
                                         child: const Text('Continue'),
                                       ),
                                     ),
-                                  ),
-                                ),
+                                  ),                                
+                                ), 
                               ],
                             ),
                           );
-                        }),
+                         
+                        }
+                    ),
                   ),
                 ),
               ),
@@ -460,19 +537,15 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
     });
 
     try {
-      final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
-      //cleaned form fields
       final origin = normalizeUrl(_originController.text.trim());
-      final username = _usernameController.text.trim();
       final String enteredPassword = _passwordController.text.trim();
       final String? savedPassword = widget.password;
-
+      final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
+      final username = _usernameController.text.trim();
       final password = ticket.isNotEmpty ? ticket : enteredPassword;
       final realm = _selectedDomain?.realm ?? mRealm;
-
       var client = await proxclient.authenticate(
-          '$username@$realm', password, origin, settings.sslValidation!);
-
+            '$username@$realm', password, origin, settings.sslValidation!);
       if (client.credentials.tfa != null &&
           client.credentials.tfa!.kinds().isNotEmpty) {
         if (!mounted) return;
@@ -566,7 +639,6 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
         Navigator.of(context).pop(client);
       }
     } on proxclient.ProxmoxApiException catch (e) {
-      print(e);
       if (!mounted) return;
       if (e.message.contains('No ticket')) {
         showDialog(
@@ -703,8 +775,12 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
 
     setState(() {
       _progressModel.inProgress -= 1;
+      if(response![0]!.comment == null){
+        isOIDC = true;
+      }
+      else {}
       _selectedDomain = selection;
-    });
+    }); 
 
     return response;
   }
@@ -847,3 +923,89 @@ Uri normalizeUrl(String urlText) {
 
   return Uri.https(urlText, '');
 }
+
+
+class OIDCAuthWidget extends StatefulWidget {
+  final String host;
+  final String realm;
+  final String redirectUrl;
+  final AuthCallBack authHandler;
+
+  OIDCAuthWidget(
+    {
+      required this.host,
+      required this.realm,
+      required this.authHandler,
+      required this.redirectUrl
+    }
+  );
+
+  OIDCAuthWidgetState createState() => OIDCAuthWidgetState();
+}
+class OIDCAuthWidgetState extends State<OIDCAuthWidget>{
+  final GlobalKey webViewKey = GlobalKey();
+  late InAppWebViewController webController;
+  late Future<String> authUrl;
+  InAppWebViewSettings settings = InAppWebViewSettings(
+    useShouldOverrideUrlLoading: true
+  );
+
+  void initState(){
+    super.initState();
+    authUrl = fetchOIDCAuthUrl(
+      widget.realm,
+      widget.host,
+      widget.redirectUrl
+    );
+  }
+
+  Widget build(BuildContext context){
+    return FutureBuilder<String>(
+      future: this.authUrl,
+      builder: (context, snapshot) {
+        if (snapshot.connectionState == ConnectionState.done){
+            String data = snapshot.data!;
+            if (data == ""){
+              return Scaffold(
+                body: Center(
+                  child: Text(
+                    "Data could not be loaded."
+                  )
+                )
+              );
+            }
+            else {
+              String fetchedUrl = snapshot.data!; 
+              return Scaffold(body:InAppWebView(
+                key: webViewKey,
+                initialUrlRequest: URLRequest(
+                  url: WebUri(
+                    fetchedUrl
+                  )
+                ),
+                initialSettings: settings,
+                onWebViewCreated: (controller){
+                  webController = controller;
+                },                 
+                shouldOverrideUrlLoading: (controller, navAction) async{
+                  await widget.authHandler(controller, navAction);
+                  Navigator.pop(context);
+                }
+              ));
+            }
+        }
+        return Scaffold(
+          body: Center(
+            child: Text(
+              "Loading..",
+              style: TextStyle(
+                fontSize: (MediaQuery.of(context).size.width/100.0)*5,
+                fontWeight: FontWeight.bold,
+              )
+            )
+          )
+        ); 
+      } 
+    );
+  }
+}
-- 
2.39.5



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


  parent reply	other threads:[~2025-04-29 15:08 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-04-29 15:07 [pve-devel] [PATCH manager/proxmox{_dart_api_client, _login_manager} 0/3] fix #4281 Alexander Abraham
2025-04-29 15:07 ` [pve-devel] [PATCH proxmox_dart_api_client 1/1] fix #4281: dart_api_client: Added functions for login with Open ID Alexander Abraham
2025-04-29 15:07 ` Alexander Abraham [this message]
2025-04-29 15:07 ` [pve-devel] [PATCH manager 1/1] fix #4281: manager: Enabled logging in " Alexander Abraham
2025-04-30  8:32   ` Christoph Heiss

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=20250429150757.130996-3-a.abraham@proxmox.com \
    --to=a.abraham@proxmox.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