* [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
* [pve-devel] [PATCH v2 dart-login-manager] support new TFA login flow
2021-12-14 9:08 [pve-devel] [PATCH v2 dart-client] switch to new authentication API Wolfgang Bumiller
@ 2021-12-14 9:08 ` Wolfgang Bumiller
0 siblings, 0 replies; 2+ messages in thread
From: Wolfgang Bumiller @ 2021-12-14 9:08 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>
---
Changes to v1:
* Support the old TFA API by checking if the tfa challenge's `oldApi`
property is set. Therefore the tfaType property of the tfa form is
now optional.
lib/proxmox_login_form.dart | 92 +++++++++++++++++++++++++++++++++----
lib/proxmox_tfa_form.dart | 17 +++++--
2 files changed, 97 insertions(+), 12 deletions(-)
diff --git a/lib/proxmox_login_form.dart b/lib/proxmox_login_form.dart
index 59a9f61..32f8741 100644
--- a/lib/proxmox_login_form.dart
+++ b/lib/proxmox_login_form.dart
@@ -398,12 +398,85 @@ 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.oldApi) {
+ client = await Navigator.of(context).push(MaterialPageRoute(
+ builder: (context) => ProxmoxTfaForm(
+ apiClient: client,
+ tfaType: null,
+ message: 'Enter your TFA code',
+ ),
+ ));
+ } else {
+ 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 +539,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..fcfb0e2 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,
+ required this.tfaType,
+ required 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,
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-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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox