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 <[email protected]> --- 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 _______________________________________________ pve-devel mailing list [email protected] https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
