From: Tim Marx <t.marx@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH proxmox_login_manager] add option for local biometric authentication
Date: Wed, 30 Sep 2020 12:32:43 +0200 [thread overview]
Message-ID: <20200930103243.1200300-2-t.marx@proxmox.com> (raw)
In-Reply-To: <20200930103243.1200300-1-t.marx@proxmox.com>
Signed-off-by: Tim Marx <t.marx@proxmox.com>
---
lib/proxmox_general_settings_form.dart | 39 +++-
lib/proxmox_general_settings_model.dart | 12 +-
lib/proxmox_login_form.dart | 9 +-
lib/proxmox_login_model.dart | 4 +-
lib/proxmox_login_selector.dart | 234 +++++++++++++++---------
pubspec.lock | 14 ++
pubspec.yaml | 1 +
7 files changed, 216 insertions(+), 97 deletions(-)
diff --git a/lib/proxmox_general_settings_form.dart b/lib/proxmox_general_settings_form.dart
index f3fdd44..61a7d76 100644
--- a/lib/proxmox_general_settings_form.dart
+++ b/lib/proxmox_general_settings_form.dart
@@ -1,5 +1,9 @@
+import 'dart:io';
+
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import 'package:proxmox_login_manager/proxmox_general_settings_model.dart';
+import 'package:local_auth/local_auth.dart';
class ProxmoxGeneralSettingsForm extends StatefulWidget {
@override
@@ -43,7 +47,40 @@ class _ProxmoxGeneralSettingsFormState
ProxmoxGeneralSettingsModel.fromLocalStorage();
});
},
- )
+ ),
+ if (Platform.isAndroid)
+ SwitchListTile(
+ title: Text('Biometric lock'),
+ subtitle: Text('Lock app with fingerprint'),
+ value: settings.useBiometrics,
+ onChanged: (value) async {
+ try {
+ bool didAuthenticate = await LocalAuthentication()
+ .authenticateWithBiometrics(
+ localizedReason:
+ 'Please authenticate to enable app lock');
+ if (didAuthenticate) {
+ await settings
+ .rebuild((b) => b.useBiometrics = value)
+ .toLocalStorage();
+ setState(() {
+ _settings = ProxmoxGeneralSettingsModel
+ .fromLocalStorage();
+ });
+ }
+ } on PlatformException catch (e) {
+ print(e);
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: Text('Sensor Error'),
+ content:
+ Text('Accessing biometric sensor failed'),
+ ),
+ );
+ }
+ },
+ )
],
),
);
diff --git a/lib/proxmox_general_settings_model.dart b/lib/proxmox_general_settings_model.dart
index 72fc0f7..3f3b9a3 100644
--- a/lib/proxmox_general_settings_model.dart
+++ b/lib/proxmox_general_settings_model.dart
@@ -11,6 +11,7 @@ abstract class ProxmoxGeneralSettingsModel
implements
Built<ProxmoxGeneralSettingsModel, ProxmoxGeneralSettingsModelBuilder> {
bool get sslValidation;
+ bool get useBiometrics;
ProxmoxGeneralSettingsModel._();
factory ProxmoxGeneralSettingsModel(
@@ -18,8 +19,15 @@ abstract class ProxmoxGeneralSettingsModel
_$ProxmoxGeneralSettingsModel;
factory ProxmoxGeneralSettingsModel.defaultValues() =>
- ProxmoxGeneralSettingsModel((b) => b..sslValidation = true);
-
+ ProxmoxGeneralSettingsModel(
+ (b) => b
+ ..sslValidation = true
+ ..useBiometrics = false,
+ );
+ static void _initializeBuilder(ProxmoxGeneralSettingsModelBuilder builder) =>
+ builder
+ ..sslValidation = true
+ ..useBiometrics = false;
Object toJson() {
return serializers.serializeWith(
ProxmoxGeneralSettingsModel.serializer, this);
diff --git a/lib/proxmox_login_form.dart b/lib/proxmox_login_form.dart
index 3115ffd..50afb3a 100644
--- a/lib/proxmox_login_form.dart
+++ b/lib/proxmox_login_form.dart
@@ -556,8 +556,10 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
}
class ProxmoxProgressOverlay extends StatelessWidget {
+ final Color foregroundColor;
const ProxmoxProgressOverlay({
Key key,
+ Color this.foregroundColor,
@required this.message,
}) : super(key: key);
@@ -574,13 +576,16 @@ class ProxmoxProgressOverlay extends StatelessWidget {
Text(
message,
style: TextStyle(
- color: Theme.of(context).accentColor,
+ color: foregroundColor ?? Theme.of(context).accentColor,
fontSize: 20,
),
),
Padding(
padding: const EdgeInsets.only(top: 20.0),
- child: CircularProgressIndicator(),
+ child: CircularProgressIndicator(
+ valueColor: AlwaysStoppedAnimation<Color>(
+ foregroundColor ?? Theme.of(context).accentColor),
+ ),
)
],
),
diff --git a/lib/proxmox_login_model.dart b/lib/proxmox_login_model.dart
index af7c4fd..067e92f 100644
--- a/lib/proxmox_login_model.dart
+++ b/lib/proxmox_login_model.dart
@@ -44,8 +44,10 @@ abstract class ProxmoxLoginStorage
}
Future<proxclient.ProxmoxApiClient> recoverLatestSession() async {
- final latestSession = logins.singleWhere((e) => e.ticket.isNotEmpty);
final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
+ if (settings.useBiometrics)
+ throw Exception('Biometric authentication required');
+ final latestSession = logins.singleWhere((e) => e.ticket.isNotEmpty);
final apiClient = await proxclient.authenticate(latestSession.fullUsername,
latestSession.ticket, latestSession.origin, settings.sslValidation);
return apiClient;
diff --git a/lib/proxmox_login_selector.dart b/lib/proxmox_login_selector.dart
index f6ca2fa..fc8dda9 100644
--- a/lib/proxmox_login_selector.dart
+++ b/lib/proxmox_login_selector.dart
@@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:local_auth/local_auth.dart';
import 'package:proxmox_login_manager/proxmox_general_settings_form.dart';
+import 'package:proxmox_login_manager/proxmox_general_settings_model.dart';
import 'package:proxmox_login_manager/proxmox_login_form.dart';
import 'package:proxmox_login_manager/proxmox_login_model.dart';
import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart'
@@ -19,12 +22,44 @@ class ProxmoxLoginSelector extends StatefulWidget {
class _ProxmoxLoginSelectorState extends State<ProxmoxLoginSelector> {
Future<ProxmoxLoginStorage> loginStorage;
+ Future<bool> authenticated;
+ final LocalAuthentication auth = LocalAuthentication();
+ bool _isAuthenticating = false;
+
@override
void initState() {
super.initState();
+ authenticated = _authenticate();
loginStorage = ProxmoxLoginStorage.fromLocalStorage();
}
+ Future<bool> _authenticate() async {
+ final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
+
+ if (!settings.useBiometrics) return true;
+
+ var authenticated = false;
+
+ try {
+ setState(() {
+ _isAuthenticating = true;
+ });
+ while (!authenticated) {
+ authenticated = await auth.authenticateWithBiometrics(
+ localizedReason: 'Scan your fingerprint to authenticate',
+ useErrorDialogs: true,
+ stickyAuth: true);
+ }
+ setState(() {
+ _isAuthenticating = false;
+ });
+ } on PlatformException catch (e) {
+ print(e);
+ }
+ if (!mounted) return authenticated;
+ return authenticated;
+ }
+
@override
Widget build(BuildContext context) {
return SafeArea(
@@ -57,103 +92,120 @@ class _ProxmoxLoginSelectorState extends State<ProxmoxLoginSelector> {
})
],
),
- body: FutureBuilder<ProxmoxLoginStorage>(
- future: loginStorage,
+ body: FutureBuilder(
+ future: authenticated,
builder: (context, snapshot) {
- if (!snapshot.hasData) {
- return Center(
- child: CircularProgressIndicator(),
- );
- }
- if (snapshot.hasData && (snapshot.data.logins?.isEmpty ?? true)) {
- return Center(
- child: Text('Add an account'),
+ if (_isAuthenticating || !snapshot.hasData || !snapshot.data) {
+ return ProxmoxProgressOverlay(
+ message: 'Waiting for authentication',
+ foregroundColor: Colors.white,
);
}
- var items = <Widget>[];
- final logins = snapshot.data?.logins;
+ return FutureBuilder<ProxmoxLoginStorage>(
+ future: loginStorage,
+ builder: (context, snapshot) {
+ if (!snapshot.hasData) {
+ return Center(
+ child: CircularProgressIndicator(),
+ );
+ }
+ if (snapshot.hasData &&
+ (snapshot.data.logins?.isEmpty ?? true)) {
+ return Center(
+ child: Text('Add an account'),
+ );
+ }
+ var items = <Widget>[];
+ final logins = snapshot.data?.logins;
- final activeSessions =
- logins.rebuild((b) => b.where((b) => b.activeSession));
+ final activeSessions =
+ logins.rebuild((b) => b.where((b) => b.activeSession));
- if (activeSessions.isNotEmpty) {
- items.addAll([
- Padding(
- padding: const EdgeInsets.all(12.0),
- child: Text(
- 'Active Sessions',
- style: TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.bold,
+ if (activeSessions.isNotEmpty) {
+ items.addAll([
+ Padding(
+ padding: const EdgeInsets.all(12.0),
+ child: Text(
+ 'Active Sessions',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ...activeSessions.map((s) => ListTile(
+ title: Text(s.fullHostname),
+ subtitle: Text(s.fullUsername),
+ trailing: Icon(Icons.navigate_next),
+ leading: PopupMenuButton(
+ icon: Icon(Icons.more_vert,
+ color: Colors.green),
+ itemBuilder: (context) => [
+ PopupMenuItem(
+ child: ListTile(
+ dense: true,
+ leading: Icon(Icons.logout),
+ title: Text('Logout'),
+ onTap: () async {
+ await snapshot.data
+ .rebuild((b) => b.logins
+ .rebuildWhere(
+ (m) => s == m,
+ (b) =>
+ b..ticket = ''))
+ .saveToDisk();
+ refreshFromStorage();
+ Navigator.of(context).pop();
+ },
+ ),
+ ),
+ ]),
+ onTap: () => _login(user: s),
+ )),
+ ]);
+ }
+ items.addAll([
+ Padding(
+ padding: const EdgeInsets.all(12.0),
+ child: Text(
+ 'Available Sites',
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
),
- ),
- ),
- ...activeSessions.map((s) => ListTile(
- title: Text(s.fullHostname),
- subtitle: Text(s.fullUsername),
- trailing: Icon(Icons.navigate_next),
- leading: PopupMenuButton(
- icon: Icon(Icons.more_vert, color: Colors.green),
- itemBuilder: (context) => [
- PopupMenuItem(
- child: ListTile(
- dense: true,
- leading: Icon(Icons.logout),
- title: Text('Logout'),
- onTap: () async {
- await snapshot.data
- .rebuild((b) => b.logins
- .rebuildWhere((m) => s == m,
- (b) => b..ticket = ''))
- .saveToDisk();
- refreshFromStorage();
- Navigator.of(context).pop();
- },
- ),
- ),
- ]),
- onTap: () => _login(user: s),
- )),
- ]);
- }
- items.addAll([
- Padding(
- padding: const EdgeInsets.all(12.0),
- child: Text(
- 'Available Sites',
- style: TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.bold,
- ),
- ),
- ),
- ...logins.where((b) => !b.activeSession)?.map((l) => ListTile(
- title: Text(l.fullHostname),
- subtitle: Text(l.fullUsername),
- trailing: Icon(Icons.navigate_next),
- leading: PopupMenuButton(
- itemBuilder: (context) => [
- PopupMenuItem(
- child: ListTile(
- dense: true,
- leading: Icon(Icons.delete),
- title: Text('Delete'),
- onTap: () async {
- await snapshot.data
- .rebuild((b) => b.logins.remove(l))
- .saveToDisk();
- refreshFromStorage();
- Navigator.of(context).pop();
- },
- ),
- ),
- ]),
- onTap: () => _login(user: l),
- ))
- ]);
- return ListView(
- children: items,
- );
+ ...logins
+ .where((b) => !b.activeSession)
+ ?.map((l) => ListTile(
+ title: Text(l.fullHostname),
+ subtitle: Text(l.fullUsername),
+ trailing: Icon(Icons.navigate_next),
+ leading: PopupMenuButton(
+ itemBuilder: (context) => [
+ PopupMenuItem(
+ child: ListTile(
+ dense: true,
+ leading: Icon(Icons.delete),
+ title: Text('Delete'),
+ onTap: () async {
+ await snapshot.data
+ .rebuild((b) =>
+ b.logins.remove(l))
+ .saveToDisk();
+ refreshFromStorage();
+ Navigator.of(context).pop();
+ },
+ ),
+ ),
+ ]),
+ onTap: () => _login(user: l),
+ ))
+ ]);
+ return ListView(
+ children: items,
+ );
+ });
}),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => _login(isCreate: true),
diff --git a/pubspec.lock b/pubspec.lock
index b0bf674..58794ad 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -209,6 +209,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ flutter_plugin_android_lifecycle:
+ dependency: transitive
+ description:
+ name: flutter_plugin_android_lifecycle
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.11"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -289,6 +296,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
+ local_auth:
+ dependency: "direct main"
+ description:
+ name: local_auth
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.6.3+2"
logging:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index dc11ec8..2b5f5a5 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -12,6 +12,7 @@ dependencies:
shared_preferences: '>=0.5.10 <2.0.0'
built_value: ^7.1.0
built_collection: ^4.3.2
+ local_auth: ^0.6.3+2
proxmox_dart_api_client:
path: ../proxmox_dart_api_client
--
2.20.1
next prev parent reply other threads:[~2020-09-30 10:32 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2020-09-30 10:32 [pve-devel] [PATCH pve_flutter_frontend] add changes to support " Tim Marx
2020-09-30 10:32 ` Tim Marx [this message]
2020-11-12 15:39 ` Aaron Lauterer
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=20200930103243.1200300-2-t.marx@proxmox.com \
--to=t.marx@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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox