[pve-devel] [PATCH dart-client] switch to new authentication API

Wolfgang Bumiller w.bumiller at proxmox.com
Mon Dec 13 13:24:03 CET 2021


and decode the tfa challenge

Signed-off-by: Wolfgang Bumiller <w.bumiller at 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





More information about the pve-devel mailing list