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

and decode the tfa challenge

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
Changes to v1:
  * Support the old login API as a fallback
    For this the API response's error is checked for whether it
    considered the 'new-format' parameter to be an error.
  * The TfaChallenge object now has an 'oldApi' member which is set if
    the `NeedTFA` property was returned from the ticket call if the
    check for the new-style TFA ticket fails.
  * The 'type' parameter in the finishTfaChallenge() & tfaChallenge()
    methods is now optional and may be null if `challenge.oldApi` is
    set.

 lib/src/authenticate.dart           | 16 ++++++-
 lib/src/client.dart                 |  8 +---
 lib/src/credentials.dart            | 66 ++++++++++++++++++++++++++---
 lib/src/handle_ticket_response.dart | 20 +++++++--
 lib/src/tfa_challenge.dart          | 40 +++++++++++++++++
 5 files changed, 133 insertions(+), 17 deletions(-)
 create mode 100644 lib/src/tfa_challenge.dart

diff --git a/lib/src/authenticate.dart b/lib/src/authenticate.dart
index 5bbcfc4..902ae56 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);
@@ -33,6 +33,20 @@ Future<ProxmoxApiClient> authenticate(
     var response = await httpClient
         .post(credentials.ticketUrl, body: body)
         .timeout(Duration(seconds: 5));
+    try {
+      credentials = handleAccessTicketResponse(response, credentials);
+    } on ProxmoxApiException catch(e) {
+      if (e.details?['new-format'] != null) {
+        // retry with old api
+        body.remove('new-format');
+        response = await httpClient
+          .post(credentials.ticketUrl, body: body)
+          .timeout(Duration(seconds: 5));
+        credentials = handleAccessTicketResponse(response, credentials);
+      } else {
+        rethrow;
+      }
+    }
 
     credentials = handleAccessTicketResponse(response, credentials);
 
diff --git a/lib/src/client.dart b/lib/src/client.dart
index 6c12191..f2f5853 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..e84bc85 100644
--- a/lib/src/credentials.dart
+++ b/lib/src/credentials.dart
@@ -1,6 +1,7 @@
 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';
@@ -20,7 +21,7 @@ class Credentials {
 
   final DateTime? expiration;
 
-  bool tfa;
+  final TfaChallenge? tfa;
 
   bool get canRefresh => ticket != null;
 
@@ -38,7 +39,7 @@ class Credentials {
     this.ticket,
     this.csrfToken,
     this.expiration,
-    this.tfa = false,
+    this.tfa = null,
   });
 
   Future<Credentials> refresh({http.Client? httpClient}) async {
@@ -59,13 +60,66 @@ class Credentials {
     return credentials;
   }
 
-  Future<Credentials> tfaChallenge(String code,
+  Future<Credentials> tfaChallenge(String? type, String code,
       {http.Client? httpClient}) async {
-    httpClient ??= getCustomIOHttpClient();
 
-    final body = {'response': code};
+    if (tfa == null) {
+      throw StateError('No tfa challange expected');
+    }
+    var tmp = this.tfa!;
+
+    var body;
+    var url;
+    if (tmp.oldApi) {
+      url = tfaUrl;
+      body = {'response': code};
+    } else {
+      if (type == null) {
+        throw StateError('No tfa type provided with new login api');
+      }
+
+      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");
+      }
+
+      url = ticketUrl;
+      body = {
+        'username': username,
+        'password': '${type}:${code}',
+        'tfa-challenge': ticket,
+        'new-format': '1',
+      };
+    }
+
+    httpClient ??= getCustomIOHttpClient();
 
-    final response = await httpClient.post(tfaUrl, body: body);
+    final response = await httpClient.post(url, 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..4224753 100644
--- a/lib/src/handle_ticket_response.dart
+++ b/lib/src/handle_ticket_response.dart
@@ -2,10 +2,13 @@ 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) {
-  response.validate(false);
+  // Full validation as we want to check for 'new-format' being unsupported via
+  // the exception's 'details'.
+  response.validate(true);
 
   final bodyJson = jsonDecode(response.body)['data'];
 
@@ -19,8 +22,18 @@ 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);
+
+  TfaChallenge? tfa = null;
+  if (ticketData != null && ticketData.startsWith("!tfa!")) {
+    tfa = TfaChallenge.fromJson(
+      jsonDecode(
+        Uri.decodeComponent(ticketData.substring(5)),
+      ),
+    );
+  } else if (bodyJson['NeedTFA'] != null && bodyJson['NeedTFA'] == 1) {
+    tfa = TfaChallenge.old();
+  }
 
   return Credentials(
     unauthenicatedCredentials.apiBaseUrl,
@@ -52,6 +65,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..0d1d868
--- /dev/null
+++ b/lib/src/tfa_challenge.dart
@@ -0,0 +1,40 @@
+class TfaChallenge {
+  final bool totp;
+  final List<int> recovery;
+  final bool yubico;
+  final dynamic? u2f;
+  final dynamic? webauthn;
+
+  final bool oldApi;
+
+  TfaChallenge(
+    this.totp,
+    this.recovery,
+    this.yubico, {
+    this.u2f = null,
+    this.webauthn = null,
+    this.oldApi = false,
+  });
+
+  TfaChallenge.old()
+    : totp = false
+    , recovery = []
+    , yubico = false
+    , u2f = null
+    , webauthn = null
+    , oldApi = true
+    ;
+
+  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']
+    , oldApi = false
+    ;
+}
-- 
2.30.2





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

end of thread, other threads:[~2021-12-14  9:08 UTC | newest]

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

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal