verify_ticket: allow general non-challenge tfa to be run as two step call
authorWolfgang Bumiller <w.bumiller@proxmox.com>
Mon, 8 Apr 2019 11:58:27 +0000 (13:58 +0200)
committerThomas Lamprecht <t.lamprecht@proxmox.com>
Mon, 8 Apr 2019 12:48:13 +0000 (14:48 +0200)
This allows for doing OTP TFA in two steps, first login with normal
credentials and get the half-logged-in ticket, then send the OTP
verification for full login, same as with u2f was already possible.

This allows for a nicer UI, as OTP fields can be shown on demand, and
do not need to be visible by default.

The old way of sending the OTP code immediately with the initial
credentials request still works for backward compatibility and as
some API user may prefer it.

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
PVE/API2/AccessControl.pm
PVE/AccessControl.pm

index f336696..2caa4af 100644 (file)
@@ -125,23 +125,32 @@ my $verify_auth = sub {
 my $create_ticket = sub {
     my ($rpcenv, $username, $pw_or_ticket, $otp) = @_;
 
-    my ($ticketuser, $u2fdata);
-    if (($ticketuser = PVE::AccessControl::verify_ticket($pw_or_ticket, 1)) &&
-       ($ticketuser eq 'root@pam' || $ticketuser eq $username)) {
+    my ($ticketuser, undef, $tfa_info) = PVE::AccessControl::verify_ticket($pw_or_ticket, 1);
+    if (defined($ticketuser) && ($ticketuser eq 'root@pam' || $ticketuser eq $username)) {
+       if (defined($tfa_info)) {
+           die "incomplete ticket\n";
+       }
        # valid ticket. Note: root@pam can create tickets for other users
     } else {
-       ($username, $u2fdata) = PVE::AccessControl::authenticate_user($username, $pw_or_ticket, $otp);
+       ($username, $tfa_info) = PVE::AccessControl::authenticate_user($username, $pw_or_ticket, $otp);
     }
 
     my %extra;
     my $ticket_data = $username;
-    if (defined($u2fdata)) {
-       my $u2f = get_u2f_instance($rpcenv, $u2fdata->@{qw(publicKey keyHandle)});
-       my $challenge = $u2f->auth_challenge()
-           or die "failed to get u2f challenge\n";
-       $challenge = decode_json($challenge);
-       $extra{U2FChallenge} = $challenge;
-       $ticket_data = "u2f!$username!$challenge->{challenge}";
+    if (defined($tfa_info)) {
+       $extra{NeedTFA} = 1;
+       if ($tfa_info->{type} eq 'u2f') {
+           my $u2finfo = $tfa_info->{data};
+           my $u2f = get_u2f_instance($rpcenv, $u2finfo->@{qw(publicKey keyHandle)});
+           my $challenge = $u2f->auth_challenge()
+               or die "failed to get u2f challenge\n";
+           $challenge = decode_json($challenge);
+           $extra{U2FChallenge} = $challenge;
+           $ticket_data = "u2f!$username!$challenge->{challenge}";
+       } else {
+           # General half-login / 'missing 2nd factor' ticket:
+           $ticket_data = "tfa!$username";
+       }
     }
 
     my $ticket = PVE::AccessControl::assemble_ticket($ticket_data);
@@ -302,7 +311,7 @@ __PACKAGE__->register_method ({
        }
 
        $res->{cap} = &$compute_api_permission($rpcenv, $username)
-           if !defined($res->{U2FChallenge});
+           if !defined($res->{NeedTFA});
 
        if (PVE::Corosync::check_conf_exists(1)) {
            if ($rpcenv->check($username, '/', ['Sys.Audit'], 1)) {
@@ -616,27 +625,35 @@ __PACKAGE__->register_method({
        my ($param) = @_;
 
        my $rpcenv = PVE::RPCEnvironment::get();
-       my $challenge = $rpcenv->get_u2f_challenge()
-          or raise('no active challenge');
        my $authuser = $rpcenv->get_user();
        my ($username, undef, $realm) = PVE::AccessControl::verify_username($authuser);
 
-       my ($tfa_type, $u2fdata) = PVE::AccessControl::user_get_tfa($username, $realm);
-       if (!defined($tfa_type) || $tfa_type ne 'u2f') {
+       my ($tfa_type, $tfa_data) = PVE::AccessControl::user_get_tfa($username, $realm);
+       if (!defined($tfa_type)) {
            raise('no u2f data available');
        }
 
-       my $keyHandle = $u2fdata->{keyHandle};
-       my $publicKey = $u2fdata->{publicKey};
-       raise("incomplete u2f setup")
-           if !defined($keyHandle) || !defined($publicKey);
+       eval {
+           if ($tfa_type eq 'u2f') {
+               my $challenge = $rpcenv->get_u2f_challenge()
+                  or raise('no active challenge');
 
-       my $u2f = get_u2f_instance($rpcenv, $publicKey, $keyHandle);
-       $u2f->set_challenge($challenge);
+               my $keyHandle = $tfa_data->{keyHandle};
+               my $publicKey = $tfa_data->{publicKey};
+               raise("incomplete u2f setup")
+                   if !defined($keyHandle) || !defined($publicKey);
 
-       eval {
-           my ($counter, $present) = $u2f->auth_verify($param->{response});
-           # Do we want to do anything with these?
+               my $u2f = get_u2f_instance($rpcenv, $publicKey, $keyHandle);
+               $u2f->set_challenge($challenge);
+
+               my ($counter, $present) = $u2f->auth_verify($param->{response});
+               # Do we want to do anything with these?
+           } else {
+               # sanity check before handing off to the verification code:
+               my $keys = $tfa_data->{keys} or die "missing tfa keys\n";
+               my $config = $tfa_data->{config} or die "bad tfa entry\n";
+               PVE::AccessControl::verify_one_time_pw($tfa_type, $authuser, $keys, $config, $param->{response});
+           }
        };
        if (my $err = $@) {
            my $clientip = $rpcenv->get_client_ip() || '';
@@ -644,10 +661,8 @@ __PACKAGE__->register_method({
            die PVE::Exception->new("authentication failure\n", code => 401);
        }
 
-       # create a new ticket for the user:
-       my $ticket_data = "u2f!$authuser!verified";
        return {
-           ticket => PVE::AccessControl::assemble_ticket($ticket_data),
+           ticket => PVE::AccessControl::assemble_ticket($authuser),
            cap => &$compute_api_permission($rpcenv, $authuser),
        }
     }});
index af1bd6a..bec962f 100644 (file)
@@ -308,10 +308,10 @@ sub verify_ticket {
        return $auth_failure->();
     }
 
-    my ($username, $challenge);
+    my ($username, $tfa_info);
     if ($data =~ m{^u2f!([^!]+)!([0-9a-zA-Z/.=_\-+]+)$}) {
        # Ticket for u2f-users:
-       ($username, $challenge) = ($1, $2);
+       ($username, my $challenge) = ($1, $2);
        if ($challenge eq 'verified') {
            # u2f challenge was completed
            $challenge = undef;
@@ -320,6 +320,15 @@ sub verify_ticket {
            # so we treat this ticket as invalid:
            return $auth_failure->();
        }
+       $tfa_info = {
+           type => 'u2f',
+           challenge => $challenge,
+       };
+    } elsif ($data =~ /^tfa!(.*)$/) {
+       # TOTP and Yubico don't require a challenge so this is the generic
+       # 'missing 2nd factor ticket'
+       $username = $1;
+       $tfa_info = { type => 'tfa' };
     } else {
        # Regular ticket (full access)
        $username = $data;
@@ -327,7 +336,7 @@ sub verify_ticket {
 
     return undef if !PVE::Auth::Plugin::verify_username($username, $noerr);
 
-    return wantarray ? ($username, $age, $challenge) : $username;
+    return wantarray ? ($username, $age, $tfa_info) : $username;
 }
 
 # VNC tickets
@@ -515,22 +524,32 @@ sub authenticate_user {
     my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
     $plugin->authenticate_user($cfg, $realm, $ruid, $password);
 
-    my $u2f;
-
     my ($type, $tfa_data) = user_get_tfa($username, $realm);
     if ($type) {
        if ($type eq 'u2f') {
            # Note that if the user did not manage to complete the initial u2f registration
            # challenge we have a hash containing a 'challenge' entry in the user's tfa.cfg entry:
-           $u2f = $tfa_data if !exists $tfa_data->{challenge};
+           $tfa_data = undef if exists $tfa_data->{challenge};
+       } elsif (!defined($otp)) {
+           # The user requires a 2nd factor but has not provided one. Return success but
+           # don't clear $tfa_data.
        } else {
            my $keys = $tfa_data->{keys};
            my $tfa_cfg = $tfa_data->{config};
            verify_one_time_pw($type, $username, $keys, $tfa_cfg, $otp);
+           $tfa_data = undef;
+       }
+
+       # Return the type along with the rest:
+       if ($tfa_data) {
+           $tfa_data = {
+               type => $type,
+               data => $tfa_data,
+           };
        }
     }
 
-    return wantarray ? ($username, $u2f) : $username;
+    return wantarray ? ($username, $tfa_data) : $username;
 }
 
 sub domain_set_password {