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.abra...@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

Reply via email to