[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