[pve-devel] [PATCH apiclient] support new tfa api
Wolfgang Bumiller
w.bumiller at proxmox.com
Tue Nov 30 10:57:40 CET 2021
Note that in PVE we should instantiate the API client with
`pve_new_format` in order to have this client also switch to
the new mechanism, otherwise the old api will be used which
does not support multiple factors or recovery keys.
Signed-off-by: Wolfgang Bumiller <w.bumiller at proxmox.com>
---
PVE/APIClient/LWP.pm | 85 +++++++++++++++++++++++++++++++++++++++++---
1 file changed, 80 insertions(+), 5 deletions(-)
diff --git a/PVE/APIClient/LWP.pm b/PVE/APIClient/LWP.pm
index 63f2177..27ecfed 100755
--- a/PVE/APIClient/LWP.pm
+++ b/PVE/APIClient/LWP.pm
@@ -93,7 +93,7 @@ sub update_ticket {
$agent->default_header('Cookie', $cookie);
}
-sub two_factor_auth_login {
+my sub two_factor_auth_login_old : prototype($$$) {
my ($self, $type, $challenge) = @_;
if ($type eq 'PVE:tfa') {
@@ -110,6 +110,74 @@ sub two_factor_auth_login {
}
}
+my sub extra_login_params : prototype($) {
+ my ($self) = @_;
+ return $self->{pve_new_format} ? ('new-format' => 1) : ();
+}
+
+my sub two_factor_auth_login : prototype($$$) {
+ my ($self, $challenge, $ticket) = @_;
+
+ raise("TFA-enabled login currently works only with a TTY.") if !-t STDIN;
+
+ $challenge = eval { from_json($challenge, { utf8 => 1 }) };
+ if (my $err = $@) {
+ raise("Bad TFA challenge: $err");
+ }
+ raise("Bad TFA challenge!") if !$challenge;
+
+ my @available;
+ push @available, 'totp' if $challenge->{totp};
+ push @available, 'recovery' if $challenge->{recovery};
+ push @available, 'yubico' if $challenge->{yubico};
+
+ my $selected;
+ if (@available == 1) {
+ $selected = $available[0];
+ } elsif (@available > 1) {
+ while (!defined($selected)) {
+ print "Available TFA methods:\n";
+ print "$_: $available[$_]\n" for (0..(@available - 1));
+ print "Select TFA method: ";
+ STDOUT->flush;
+ my $response = <STDIN>;
+ if ($response =~ /^\s*(\d+)\s*$/) {
+ $selected = int($response);
+ }
+ }
+ $selected = $available[$selected];
+ } else {
+ raise("TFA required, but available authentication types are not supported, aborting!");
+ }
+
+ if ($selected eq 'recovery') {
+ my $keys = $challenge->{recovery};
+ if (@$keys <= 3) {
+ print("WARNING: Few recovery keys remaining: ");
+ } else {
+ print("The following recovery codes are available: ");
+ }
+ print(join(', ', @$keys), "\n");
+ }
+
+ print "Enter $selected code for user $self->{username}: ";
+ STDOUT->flush;
+ my $tfa_response = <STDIN>;
+ chomp $tfa_response;
+
+ return $self->post(
+ '/api2/json/access/ticket',
+ {
+ username => $self->{username},
+ password => "$selected:$tfa_response",
+ 'tfa-challenge' => $ticket,
+ (extra_login_params($self))
+ },
+ );
+}
+
+my $new_tfa_ticket_re = qr/^[^\s:]+:!tfa!([^:]+):/;
+my $old_tfa_ticket_re = qr/^([^\s!]+)![^!]*(!([0-9a-zA-Z\/.=_\-+]+))?$/;
sub login {
my ($self) = @_;
@@ -127,7 +195,8 @@ sub login {
my $exec_login = sub {
return $ua->post($uri, {
username => $username,
- password => $self->{password} || ''
+ password => $self->{password} || '',
+ (extra_login_params($self))
});
};
@@ -152,10 +221,15 @@ sub login {
$self->update_csrftoken($data->{CSRFPreventionToken});
# handle two-factor login
- my $tfa_ticket_re = qr/^([^\s!]+)![^!]*(!([0-9a-zA-Z\/.=_\-+]+))?$/;
- if ($data->{ticket} =~ m/$tfa_ticket_re/) {
+ my $ticket = $data->{ticket};
+ if ($ticket =~ $new_tfa_ticket_re) {
+ my $challenge = uri_unescape($1);
+ $data = two_factor_auth_login($self, $challenge, $ticket);
+ $self->update_ticket($data->{ticket});
+ } elsif ($ticket =~ $old_tfa_ticket_re) {
+ # handle old-style two-factor login for PVE:
my ($type, $challenge) = ($1, $2);
- $data = $self->two_factor_auth_login($type, $challenge);
+ $data = two_factor_auth_login_old($self, $type, $challenge);
$self->update_ticket($data->{ticket});
}
@@ -329,6 +403,7 @@ sub new {
},
register_fingerprint_cb => $param{register_fingerprint_cb},
timeout => $param{timeout} || 60,
+ pve_new_format => $param{pve_new_format},
};
bless $self, $class;
--
2.30.2
More information about the pve-devel
mailing list