From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 4DFE082801 for ; Tue, 30 Nov 2021 10:57:42 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 443E114BB9 for ; Tue, 30 Nov 2021 10:57:42 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 7CE3814BB0 for ; Tue, 30 Nov 2021 10:57:41 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 50A844380A for ; Tue, 30 Nov 2021 10:57:41 +0100 (CET) From: Wolfgang Bumiller To: pve-devel@lists.proxmox.com Date: Tue, 30 Nov 2021 10:57:40 +0100 Message-Id: <20211130095740.35774-1-w.bumiller@proxmox.com> X-Mailer: git-send-email 2.30.2 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.431 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [lwp.pm] Subject: [pve-devel] [PATCH apiclient] support new tfa api X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Tue, 30 Nov 2021 09:57:42 -0000 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 --- 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 = ; + 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 = ; + 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