From: Wolfgang Bumiller <w.bumiller@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH dart-client] switch to new authentication API
Date: Mon, 13 Dec 2021 13:24:03 +0100 [thread overview]
Message-ID: <20211213122404.84050-1-w.bumiller@proxmox.com> (raw)
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
next reply other threads:[~2021-12-13 12:24 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2021-12-13 12:24 Wolfgang Bumiller [this message]
2021-12-13 12:24 ` [pve-devel] [PATCH 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=20211213122404.84050-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.