summary |
shortlog |
log |
commit | commitdiff |
tree
raw |
patch |
inline | side by side (from parent 1:
a270d4e)
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>
my $create_ticket = sub {
my ($rpcenv, $username, $pw_or_ticket, $otp) = @_;
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 {
# 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;
}
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);
}
my $ticket = PVE::AccessControl::assemble_ticket($ticket_data);
}
$res->{cap} = &$compute_api_permission($rpcenv, $username)
}
$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)) {
if (PVE::Corosync::check_conf_exists(1)) {
if ($rpcenv->check($username, '/', ['Sys.Audit'], 1)) {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
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 $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');
}
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() || '';
};
if (my $err = $@) {
my $clientip = $rpcenv->get_client_ip() || '';
die PVE::Exception->new("authentication failure\n", code => 401);
}
die PVE::Exception->new("authentication failure\n", code => 401);
}
- # create a new ticket for the user:
- my $ticket_data = "u2f!$authuser!verified";
- ticket => PVE::AccessControl::assemble_ticket($ticket_data),
+ ticket => PVE::AccessControl::assemble_ticket($authuser),
cap => &$compute_api_permission($rpcenv, $authuser),
}
}});
cap => &$compute_api_permission($rpcenv, $authuser),
}
}});
return $auth_failure->();
}
return $auth_failure->();
}
- my ($username, $challenge);
+ my ($username, $tfa_info);
if ($data =~ m{^u2f!([^!]+)!([0-9a-zA-Z/.=_\-+]+)$}) {
# Ticket for u2f-users:
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;
if ($challenge eq 'verified') {
# u2f challenge was completed
$challenge = undef;
# so we treat this ticket as invalid:
return $auth_failure->();
}
# 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;
} else {
# Regular ticket (full access)
$username = $data;
return undef if !PVE::Auth::Plugin::verify_username($username, $noerr);
return undef if !PVE::Auth::Plugin::verify_username($username, $noerr);
- return wantarray ? ($username, $age, $challenge) : $username;
+ return wantarray ? ($username, $age, $tfa_info) : $username;
my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
$plugin->authenticate_user($cfg, $realm, $ruid, $password);
my $plugin = PVE::Auth::Plugin->lookup($cfg->{type});
$plugin->authenticate_user($cfg, $realm, $ruid, $password);
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:
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);
} 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 {
}
sub domain_set_password {