public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Wolfgang Bumiller <w.bumiller@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH apiclient] support new tfa api
Date: Tue, 30 Nov 2021 10:57:40 +0100	[thread overview]
Message-ID: <20211130095740.35774-1-w.bumiller@proxmox.com> (raw)

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@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





             reply	other threads:[~2021-11-30  9:57 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-11-30  9:57 Wolfgang Bumiller [this message]
2021-12-02 18:23 ` [pve-devel] applied: " Thomas Lamprecht

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20211130095740.35774-1-w.bumiller@proxmox.com \
    --to=w.bumiller@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal