public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH dart-client] switch to new authentication API
@ 2021-12-13 12:24 Wolfgang Bumiller
  2021-12-13 12:24 ` [pve-devel] [PATCH dart-login-manager] support new TFA login flow Wolfgang Bumiller
  0 siblings, 1 reply; 2+ messages in thread
From: Wolfgang Bumiller @ 2021-12-13 12:24 UTC (permalink / raw)
  To: pve-devel

and decode the tfa challenge

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
 lib/src/authenticate.dart           |  2 +-
 lib/src/client.dart                 |  8 +---
 lib/src/credentials.dart            | 63 ++++++++++++++++++++++++-----
 lib/src/handle_ticket_response.dart | 13 ++++--
 lib/src/tfa_challenge.dart          | 27 +++++++++++++
 5 files changed, 94 insertions(+), 19 deletions(-)
 create mode 100644 lib/src/tfa_challenge.dart

diff --git a/lib/src/authenticate.dart b/lib/src/authenticate.dart
index 5bbcfc4..e02dd96 100644
--- a/lib/src/authenticate.dart
+++ b/lib/src/authenticate.dart
@@ -25,7 +25,7 @@ Future<ProxmoxApiClient> authenticate(
 }) async {
   httpClient ??= getCustomIOHttpClient(validateSSL: validateSSL);
 
-  var body = {'username': username, 'password': password};
+  var body = {'username': username, 'password': password, 'new-format': '1'};
 
   try {
     var credentials = Credentials(apiBaseUrl, username);
diff --git a/lib/src/client.dart b/lib/src/client.dart
index 6c12191..9bdfaff 100644
--- a/lib/src/client.dart
+++ b/lib/src/client.dart
@@ -92,12 +92,8 @@ class ProxmoxApiClient extends http.BaseClient {
     return this;
   }
 
-  Future<ProxmoxApiClient> finishTfaChallenge(String code) async {
-    if (!credentials.tfa) {
-      throw StateError('No tfa challange expected');
-    }
-
-    credentials = await credentials.tfaChallenge(code, httpClient: this);
+  Future<ProxmoxApiClient> finishTfaChallenge(String type, String code) async {
+    credentials = await credentials.tfaChallenge(type, code, httpClient: this);
 
     return this;
   }
diff --git a/lib/src/credentials.dart b/lib/src/credentials.dart
index fe75e63..f8746c9 100644
--- a/lib/src/credentials.dart
+++ b/lib/src/credentials.dart
@@ -1,12 +1,12 @@
 import 'package:http/http.dart' as http;
 
 import 'package:proxmox_dart_api_client/src/handle_ticket_response.dart';
+import 'package:proxmox_dart_api_client/src/tfa_challenge.dart';
 import 'package:proxmox_dart_api_client/src/utils.dart'
     if (dart.library.html) 'utils_web.dart'
     if (dart.library.io) 'utils_native.dart';
 
 const String ticketPath = '/api2/json/access/ticket';
-const String tfaPath = '/api2/json/access/tfa';
 
 class Credentials {
   /// The URL of the authorization server
@@ -20,7 +20,7 @@ class Credentials {
 
   final DateTime? expiration;
 
-  bool tfa;
+  final TfaChallenge? tfa;
 
   bool get canRefresh => ticket != null;
 
@@ -30,15 +30,13 @@ class Credentials {
 
   Uri get ticketUrl => apiBaseUrl.replace(path: ticketPath);
 
-  Uri get tfaUrl => apiBaseUrl.replace(path: tfaPath);
-
   Credentials(
     this.apiBaseUrl,
     this.username, {
     this.ticket,
     this.csrfToken,
     this.expiration,
-    this.tfa = false,
+    this.tfa = null,
   });
 
   Future<Credentials> refresh({http.Client? httpClient}) async {
@@ -48,7 +46,11 @@ class Credentials {
       throw ArgumentError("Can't refresh credentials without valid ticket");
     }
 
-    var body = {'username': username, 'password': ticket};
+    var body = {
+      'username': username,
+      'password': ticket,
+      'new-format': '1',
+    };
 
     var response = await httpClient
         .post(ticketUrl, body: body)
@@ -59,13 +61,56 @@ class Credentials {
     return credentials;
   }
 
-  Future<Credentials> tfaChallenge(String code,
+  Future<Credentials> tfaChallenge(String type, String code,
       {http.Client? httpClient}) async {
+
+    if (tfa == null) {
+      throw StateError('No tfa challange expected');
+    }
+
+    var tmp = this.tfa!;
+
+    switch (type) {
+      case 'totp':
+        if (!tmp.totp) {
+          throw StateError("Totp challenge not available");
+        }
+        break;
+      case 'yubico':
+        if (!tmp.yubico) {
+          throw StateError("Yubico challenge not available");
+        }
+        break;
+      case 'recovery':
+        if (tmp.recovery.isEmpty) {
+          throw StateError("No recovery keys available");
+        }
+        break;
+      case 'u2f':
+        if (tmp.u2f == null) {
+          throw StateError("U2F challenge not available");
+        }
+        break;
+      case 'webauthn':
+        if (tmp.webauthn == null) {
+          throw StateError("Webauthn challenge not available");
+        }
+        break;
+      default:
+        throw StateError("unsupported tfa response type used");
+    }
+
     httpClient ??= getCustomIOHttpClient();
 
-    final body = {'response': code};
+    var body = {
+      'username': username,
+      'password': '${type}:${code}',
+      'tfa-challenge': ticket,
+      'new-format': '1',
+    };
 
-    final response = await httpClient.post(tfaUrl, body: body);
+    final response = await httpClient
+      .post(ticketUrl, body: body);
 
     final credentials = handleTfaChallengeResponse(response, this);
 
diff --git a/lib/src/handle_ticket_response.dart b/lib/src/handle_ticket_response.dart
index adcb3b1..94f15cf 100644
--- a/lib/src/handle_ticket_response.dart
+++ b/lib/src/handle_ticket_response.dart
@@ -2,6 +2,7 @@ import 'dart:convert';
 import 'package:http/http.dart' as http;
 import 'package:proxmox_dart_api_client/src/credentials.dart';
 import 'package:proxmox_dart_api_client/src/extentions.dart';
+import 'package:proxmox_dart_api_client/src/tfa_challenge.dart';
 
 Credentials handleAccessTicketResponse(
     http.Response response, Credentials unauthenicatedCredentials) {
@@ -19,8 +20,15 @@ Credentials handleAccessTicketResponse(
   final time = DateTime.fromMillisecondsSinceEpoch(
       int.parse(ticketRegex.group(3)!, radix: 16) * 1000);
 
-  final tfa =
-      bodyJson['NeedTFA'] != null && bodyJson['NeedTFA'] == 1 ? true : false;
+  final ticketData = ticketRegex.group(2);
+
+  final tfa = (ticketData != null && ticketData.startsWith("!tfa!"))
+    ? TfaChallenge.fromJson(
+        jsonDecode(
+          Uri.decodeComponent(ticketData.substring(5)),
+        ),
+      )
+    : null;
 
   return Credentials(
     unauthenicatedCredentials.apiBaseUrl,
@@ -52,6 +60,5 @@ Credentials handleTfaChallengeResponse(
     ticket: ticket,
     csrfToken: pendingTfaCredentials.csrfToken,
     expiration: time,
-    tfa: false,
   );
 }
diff --git a/lib/src/tfa_challenge.dart b/lib/src/tfa_challenge.dart
new file mode 100644
index 0000000..b92f5ee
--- /dev/null
+++ b/lib/src/tfa_challenge.dart
@@ -0,0 +1,27 @@
+class TfaChallenge {
+  final bool totp;
+  final List<int> recovery;
+  final bool yubico;
+  final dynamic? u2f;
+  final dynamic? webauthn;
+
+  TfaChallenge(
+    this.totp,
+    this.recovery,
+    this.yubico, {
+    this.u2f = null,
+    this.webauthn = null,
+  });
+
+  TfaChallenge.fromJson(Map<String, dynamic> data)
+    : totp = data['totp'] ?? false
+    , yubico = data['yubico'] ?? false
+    , recovery = (
+        data['recovery'] != null
+          ? List<int>.from(data['recovery'].map((x) => x))
+          : []
+      )
+    , u2f = data['u2f']
+    , webauthn = data['webauthn']
+    ;
+}
-- 
2.30.2





^ permalink raw reply	[flat|nested] 2+ messages in thread

* [pve-devel] [PATCH dart-login-manager] support new TFA login flow
  2021-12-13 12:24 [pve-devel] [PATCH dart-client] switch to new authentication API Wolfgang Bumiller
@ 2021-12-13 12:24 ` Wolfgang Bumiller
  0 siblings, 0 replies; 2+ messages in thread
From: Wolfgang Bumiller @ 2021-12-13 12:24 UTC (permalink / raw)
  To: pve-devel

For now this just shows a simple dialog to select the TFA type, this
can probably be switched to using tabs or a dropdown or something.

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
 lib/proxmox_login_form.dart | 78 +++++++++++++++++++++++++++++++++----
 lib/proxmox_tfa_form.dart   | 17 ++++++--
 2 files changed, 84 insertions(+), 11 deletions(-)

diff --git a/lib/proxmox_login_form.dart b/lib/proxmox_login_form.dart
index 59a9f61..da0c02f 100644
--- a/lib/proxmox_login_form.dart
+++ b/lib/proxmox_login_form.dart
@@ -398,12 +398,73 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
       var client = await proxclient.authenticate(
           '$username@$realm', password, origin, settings.sslValidation!);
 
-      if (client.credentials.tfa) {
-        client = await Navigator.of(context).push(MaterialPageRoute(
-          builder: (context) => ProxmoxTfaForm(
-            apiClient: client,
-          ),
+      if (client.credentials.tfa != null) {
+        var tfa = client.credentials.tfa!;
+
+        if (!tfa.totp && !tfa.yubico && tfa.recovery.isEmpty) {
+          return await showDialog<void>(
+            context: context,
+            builder: (context) => AlertDialog(
+              title: Text('TFA Error'),
+              content: Text('No supported TFA method available.'),
+              actions: [
+                FlatButton(
+                  onPressed: () => Navigator.of(context).pop(),
+                  child: Text('Close'),
+                ),
+              ],
+            ),
+          );
+        }
+
+        final route = (await showDialog<MaterialPageRoute>(
+          context: context,
+          builder: (BuildContext context) {
+            var buttons = <Widget>[];
+
+            void simpleTfa(String label, String tfaType, String message) {
+              buttons.add(
+                SimpleDialogOption(
+                  onPressed: () => Navigator.pop(
+                    context,
+                    MaterialPageRoute(
+                      builder: (context) => ProxmoxTfaForm(
+                        apiClient: client,
+                        tfaType: tfaType,
+                        message: message,
+                      ),
+                    ),
+                  ),
+                  child: Row(
+                    mainAxisAlignment: MainAxisAlignment.center,
+                    crossAxisAlignment: CrossAxisAlignment.center,
+                    children: [ Text(label) ],
+                  ),
+                ),
+              );
+            };
+
+            if (tfa.totp) {
+              simpleTfa('TOTP', 'totp', 'Enter your TOTP code');
+            }
+
+            if (tfa.yubico) {
+              simpleTfa('Yubico OTP', 'yubico', 'Enter your Yubico OTP code');
+            }
+
+            if (!tfa.recovery.isEmpty) {
+              simpleTfa('Recovery Code', 'recovery', 'Enter a Recovery Code');
+            }
+
+            return SimpleDialog(
+              title: const Text("Select 2nd Factor"),
+              children: buttons,
+            );
+          }
         ));
+        if (route == null)
+          return;
+        client = await Navigator.of(context).push(route!);
       }
 
       final status = await client.getClusterStatus();
@@ -466,10 +527,11 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
           builder: (context) => ProxmoxCertificateErrorDialog(),
         );
       }
+    } finally {
+      setState(() {
+        _progressModel.inProgress = false;
+      });
     }
-    setState(() {
-      _progressModel.inProgress = false;
-    });
   }
 
   Future<List<PveAccessDomainModel?>?> _getAccessDomains() async {
diff --git a/lib/proxmox_tfa_form.dart b/lib/proxmox_tfa_form.dart
index db3cfa7..c9eea49 100644
--- a/lib/proxmox_tfa_form.dart
+++ b/lib/proxmox_tfa_form.dart
@@ -1,11 +1,19 @@
 import 'package:flutter/material.dart';
 import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart';
+import 'package:proxmox_dart_api_client/src/tfa_challenge.dart';
 import 'package:proxmox_login_manager/proxmox_login_form.dart';
 
 class ProxmoxTfaForm extends StatefulWidget {
   final ProxmoxApiClient? apiClient;
+  final String? tfaType;
+  final String? message;
 
-  const ProxmoxTfaForm({Key? key, this.apiClient}) : super(key: key);
+  const ProxmoxTfaForm({
+    Key? key,
+    this.apiClient,
+    this.tfaType,
+    this.message,
+  }) : super(key: key);
 
   @override
   _ProxmoxTfaFormState createState() => _ProxmoxTfaFormState();
@@ -57,7 +65,7 @@ class _ProxmoxTfaFormState extends State<ProxmoxTfaForm> {
                             fontWeight: FontWeight.bold),
                       ),
                       Text(
-                        'Check your second factor provider',
+                        widget.message!, //'Check your second factor provider',
                         style: TextStyle(
                             color: Colors.white38, fontWeight: FontWeight.bold),
                       ),
@@ -108,7 +116,10 @@ class _ProxmoxTfaFormState extends State<ProxmoxTfaForm> {
     });
     try {
       final client =
-          await widget.apiClient!.finishTfaChallenge(_codeController.text);
+          await widget.apiClient!.finishTfaChallenge(
+            widget.tfaType!,
+            _codeController.text,
+          );
       Navigator.of(context).pop(client);
     } on ProxmoxApiException catch (e) {
       showDialog(
-- 
2.30.2





^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2021-12-13 12:24 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-12-13 12:24 [pve-devel] [PATCH dart-client] switch to new authentication API Wolfgang Bumiller
2021-12-13 12:24 ` [pve-devel] [PATCH dart-login-manager] support new TFA login flow Wolfgang Bumiller

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal