* [pve-devel] [PATCH dart-client] switch to new authentication API
@ 2021-12-13 12:24 Wolfgang Bumiller
2021-12-13 12:24 ` [pve-devel] [PATCH dart-login-manager] support new TFA login flow Wolfgang Bumiller
0 siblings, 1 reply; 2+ messages in thread
From: Wolfgang Bumiller @ 2021-12-13 12:24 UTC (permalink / raw)
To: pve-devel
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
^ permalink raw reply [flat|nested] 2+ messages in thread
* [pve-devel] [PATCH dart-login-manager] support new TFA login flow
2021-12-13 12:24 [pve-devel] [PATCH dart-client] switch to new authentication API Wolfgang Bumiller
@ 2021-12-13 12:24 ` Wolfgang Bumiller
0 siblings, 0 replies; 2+ messages in thread
From: Wolfgang Bumiller @ 2021-12-13 12:24 UTC (permalink / raw)
To: pve-devel
For now this just shows a simple dialog to select the TFA type, this
can probably be switched to using tabs or a dropdown or something.
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
lib/proxmox_login_form.dart | 78 +++++++++++++++++++++++++++++++++----
lib/proxmox_tfa_form.dart | 17 ++++++--
2 files changed, 84 insertions(+), 11 deletions(-)
diff --git a/lib/proxmox_login_form.dart b/lib/proxmox_login_form.dart
index 59a9f61..da0c02f 100644
--- a/lib/proxmox_login_form.dart
+++ b/lib/proxmox_login_form.dart
@@ -398,12 +398,73 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
var client = await proxclient.authenticate(
'$username@$realm', password, origin, settings.sslValidation!);
- if (client.credentials.tfa) {
- client = await Navigator.of(context).push(MaterialPageRoute(
- builder: (context) => ProxmoxTfaForm(
- apiClient: client,
- ),
+ if (client.credentials.tfa != null) {
+ var tfa = client.credentials.tfa!;
+
+ if (!tfa.totp && !tfa.yubico && tfa.recovery.isEmpty) {
+ return await showDialog<void>(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: Text('TFA Error'),
+ content: Text('No supported TFA method available.'),
+ actions: [
+ FlatButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+
+ final route = (await showDialog<MaterialPageRoute>(
+ context: context,
+ builder: (BuildContext context) {
+ var buttons = <Widget>[];
+
+ void simpleTfa(String label, String tfaType, String message) {
+ buttons.add(
+ SimpleDialogOption(
+ onPressed: () => Navigator.pop(
+ context,
+ MaterialPageRoute(
+ builder: (context) => ProxmoxTfaForm(
+ apiClient: client,
+ tfaType: tfaType,
+ message: message,
+ ),
+ ),
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [ Text(label) ],
+ ),
+ ),
+ );
+ };
+
+ if (tfa.totp) {
+ simpleTfa('TOTP', 'totp', 'Enter your TOTP code');
+ }
+
+ if (tfa.yubico) {
+ simpleTfa('Yubico OTP', 'yubico', 'Enter your Yubico OTP code');
+ }
+
+ if (!tfa.recovery.isEmpty) {
+ simpleTfa('Recovery Code', 'recovery', 'Enter a Recovery Code');
+ }
+
+ return SimpleDialog(
+ title: const Text("Select 2nd Factor"),
+ children: buttons,
+ );
+ }
));
+ if (route == null)
+ return;
+ client = await Navigator.of(context).push(route!);
}
final status = await client.getClusterStatus();
@@ -466,10 +527,11 @@ class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
builder: (context) => ProxmoxCertificateErrorDialog(),
);
}
+ } finally {
+ setState(() {
+ _progressModel.inProgress = false;
+ });
}
- setState(() {
- _progressModel.inProgress = false;
- });
}
Future<List<PveAccessDomainModel?>?> _getAccessDomains() async {
diff --git a/lib/proxmox_tfa_form.dart b/lib/proxmox_tfa_form.dart
index db3cfa7..c9eea49 100644
--- a/lib/proxmox_tfa_form.dart
+++ b/lib/proxmox_tfa_form.dart
@@ -1,11 +1,19 @@
import 'package:flutter/material.dart';
import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart';
+import 'package:proxmox_dart_api_client/src/tfa_challenge.dart';
import 'package:proxmox_login_manager/proxmox_login_form.dart';
class ProxmoxTfaForm extends StatefulWidget {
final ProxmoxApiClient? apiClient;
+ final String? tfaType;
+ final String? message;
- const ProxmoxTfaForm({Key? key, this.apiClient}) : super(key: key);
+ const ProxmoxTfaForm({
+ Key? key,
+ this.apiClient,
+ this.tfaType,
+ this.message,
+ }) : super(key: key);
@override
_ProxmoxTfaFormState createState() => _ProxmoxTfaFormState();
@@ -57,7 +65,7 @@ class _ProxmoxTfaFormState extends State<ProxmoxTfaForm> {
fontWeight: FontWeight.bold),
),
Text(
- 'Check your second factor provider',
+ widget.message!, //'Check your second factor provider',
style: TextStyle(
color: Colors.white38, fontWeight: FontWeight.bold),
),
@@ -108,7 +116,10 @@ class _ProxmoxTfaFormState extends State<ProxmoxTfaForm> {
});
try {
final client =
- await widget.apiClient!.finishTfaChallenge(_codeController.text);
+ await widget.apiClient!.finishTfaChallenge(
+ widget.tfaType!,
+ _codeController.text,
+ );
Navigator.of(context).pop(client);
} on ProxmoxApiException catch (e) {
showDialog(
--
2.30.2
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2021-12-13 12:24 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-12-13 12:24 [pve-devel] [PATCH dart-client] switch to new authentication API Wolfgang Bumiller
2021-12-13 12:24 ` [pve-devel] [PATCH 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.