all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Wolfgang Bumiller <w.bumiller@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH v2 dart-client] switch to new authentication API
Date: Tue, 14 Dec 2021 10:08:13 +0100	[thread overview]
Message-ID: <20211214090814.45225-1-w.bumiller@proxmox.com> (raw)

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





             reply	other threads:[~2021-12-14  9:08 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-12-14  9:08 Wolfgang Bumiller [this message]
2021-12-14  9:08 ` [pve-devel] [PATCH v2 dart-login-manager] support new TFA login flow Wolfgang Bumiller

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=20211214090814.45225-1-w.bumiller@proxmox.com \
    --to=w.bumiller@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 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