[pve-devel] [PATCH proxmox_login_manager] add option for local biometric authentication
Tim Marx
t.marx at proxmox.com
Wed Sep 30 12:32:43 CEST 2020
Signed-off-by: Tim Marx <t.marx at 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
More information about the pve-devel
mailing list