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